Merge branch 'gcarq-develop' into develop

This commit is contained in:
Gert 2018-06-01 19:32:49 -07:00
commit 24a8a57643
96 changed files with 3334 additions and 2176 deletions

View File

@ -4,3 +4,12 @@ Dockerfile
.dockerignore
config.json*
*.sqlite
.coveragerc
.eggs
.github
.pylintrc
.travis.yml
CONTRIBUTING.md
MANIFEST.in
README.md
freqtrade.service

2
.gitignore vendored
View File

@ -1,7 +1,7 @@
# Freqtrade rules
freqtrade/tests/testdata/*.json
hyperopt_conf.py
config.json
config*.json
*.sqlite
.hyperopt
logfile.txt

View File

@ -56,24 +56,19 @@ Windows, macOS and Linux
- [x] **Persistence**: Persistence is achieved through sqlite
- [x] **Dry-run**: Run the bot without playing money.
- [x] **Backtesting**: Run a simulation of your buy/sell strategy.
- [x] **Strategy Optimization**: Optimize your buy/sell strategy
parameters with Hyperopts.
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you
want to trade.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you
want to avoid.
- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell
strategy parameters with real exchange data.
- [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade.
- [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid.
- [x] **Manageable via Telegram**: Manage the bot with Telegram
- [x] **Display profit/loss in fiat**: Display your profit/loss in
33 fiat.
- [x] **Daily summary of profit/loss**: Provide a daily summary
of your profit/loss.
- [x] **Performance status report**: Provide a performance status of
your current trades.
- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat.
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
- [x] **Performance status report**: Provide a performance status of your current trades.
### Exchange supported
- [x] Bittrex
- [ ] Binance
- [ ] Others
### Exchange marketplaces supported
- [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/)
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
## Quick start
This quick start section is a very short explanation on how to test the
@ -144,8 +139,9 @@ to understand the requirements before sending your pull-requests.
### Bot commands
```bash
usage: main.py [-h] [-v] [--version] [-c PATH] [--dry-run-db] [--datadir PATH]
[--dynamic-whitelist [INT]]
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]]
[--dry-run-db]
{backtesting,hyperopt} ...
Simple High Frequency Trading Bot for crypto currencies
@ -161,13 +157,18 @@ optional arguments:
--version show program's version number and exit
-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
-d PATH, --datadir PATH
path to backtest data (default:
freqtrade/tests/testdata
-s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path
--dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies)
--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.
```
More details on:
- [How to run the bot](https://github.com/gcarq/freqtrade/blob/develop/docs/bot-usage.md#bot-commands)

View File

@ -3,6 +3,7 @@
"stake_currency": "BTC",
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"ticker_interval" : "5m",
"dry_run": false,
"unfilledtimeout": 600,
"bid_strategy": {
@ -13,19 +14,19 @@
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
],
"pair_blacklist": [
"BTC_DOGE"
"DOGE/BTC"
]
},
"experimental": {

View File

@ -4,7 +4,7 @@
"stake_amount": 0.05,
"fiat_display_currency": "USD",
"dry_run": false,
"ticker_interval": 5,
"ticker_interval": "5m",
"minimal_roi": {
"40": 0.0,
"30": 0.01,
@ -21,19 +21,19 @@
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
],
"pair_blacklist": [
"BTC_DOGE"
"DOGE/BTC"
]
},
"experimental": {

View File

@ -33,10 +33,10 @@ python3 ./freqtrade/main.py backtesting --realistic-simulation
**With 1 min tickers**
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1
python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m
```
**Reload your testdata files**
**Update cached pairs with the latest data**
```bash
python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached
```
@ -53,9 +53,9 @@ python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180
**With a (custom) strategy file**
```bash
python3 ./freqtrade/main.py -s currentstrategy backtesting
python3 ./freqtrade/main.py -s TestStrategy backtesting
```
Where `-s currentstrategy` refers to a filename `currentstrategy.py` in `freqtrade/user_data/strategies`
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory
**Exporting trades to file**
```bash
@ -80,12 +80,9 @@ 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`
- Use tickframes till 2018/01/31: `--timerange=-20180131`
- Use tickframes since 2018/01/31: `--timerange=20180131-`
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
**Update testdata directory**
@ -117,16 +114,16 @@ A backtesting result will look like that:
====================== BACKTESTING REPORT ================================
pair buy count avg profit % total profit BTC avg duration
-------- ----------- -------------- ------------------ --------------
BTC_ETH 56 -0.67 -0.00075455 62.3
BTC_LTC 38 -0.48 -0.00036315 57.9
BTC_ETC 42 -1.15 -0.00096469 67.0
BTC_DASH 72 -0.62 -0.00089368 39.9
BTC_ZEC 45 -0.46 -0.00041387 63.2
BTC_XLM 24 -0.88 -0.00041846 47.7
BTC_NXT 24 0.68 0.00031833 40.2
BTC_POWR 35 0.98 0.00064887 45.3
BTC_ADA 43 -0.39 -0.00032292 55.0
BTC_XMR 40 -0.40 -0.00032181 47.4
ETH/BTC 56 -0.67 -0.00075455 62.3
LTC/BTC 38 -0.48 -0.00036315 57.9
ETC/BTC 42 -1.15 -0.00096469 67.0
DASH/BTC 72 -0.62 -0.00089368 39.9
ZEC/BTC 45 -0.46 -0.00041387 63.2
XLM/BTC 24 -0.88 -0.00041846 47.7
NXT/BTC 24 0.68 0.00031833 40.2
POWR/BTC 35 0.98 0.00064887 45.3
ADA/BTC 43 -0.39 -0.00032292 55.0
XMR/BTC 40 -0.40 -0.00032181 47.4
TOTAL 419 -0.41 -0.00348593 52.9
```

View File

@ -9,7 +9,8 @@ it.
## Bot commands
```
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
usage: main.py [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME]
[--strategy-path PATH] [--dynamic-whitelist [INT]]
[--dry-run-db]
{backtesting,hyperopt} ...
@ -26,17 +27,18 @@ optional arguments:
--version show program's version number and exit
-c PATH, --config PATH
specify configuration file (default: config.json)
-d PATH, --datadir PATH
path to backtest data (default:
freqtrade/tests/testdata
-s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path
--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)
--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.
```
### How to use a different config file?
@ -123,13 +125,13 @@ optional arguments:
-h, --help show this help message and exit
-l, --live using live data
-i INT, --ticker-interval INT
specify ticker interval in minutes (default: 5)
specify ticker interval (default: '5m')
--realistic-simulation
uses max_open_trades from config to simulate real
world limitations
-r, --refresh-pairs-cached
refresh the pairs files in tests/testdata with
the latest data from Bittrex. Use it if you want
the latest data from the exchange. Use it if you want
to run your backtesting with up-to-date data.
```

View File

@ -17,14 +17,14 @@ 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
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default 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 | 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.
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
@ -79,6 +79,18 @@ use the `last` price and values between those interpolate between ask and last
price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary.
### What values for exchange.name?
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested
with only Bittrex and Binance.
The bot was tested with the following exchanges:
- [Bittrex](https://bittrex.com/): "bittrex"
- [Binance](https://www.binance.com/): "binance"
Feel free to test other exchanges and submit your PR to improve the bot.
### What values for fiat_display_currency?
`fiat_display_currency` set the fiat to use for the conversion form coin to fiat in Telegram.
The valid value are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
@ -96,7 +108,7 @@ creating trades.
"dry_run": true,
```
3. Remove your Bittrex API key (change them by fake api credentials)
3. Remove your Exchange API key (change them by fake api credentials)
```json
"exchange": {
"name": "bittrex",
@ -122,7 +134,7 @@ you run it in production mode.
"dry_run": false,
```
3. Insert your Bittrex API key (change them by fake api keys)
3. Insert your Exchange API key (change them by fake api keys)
```json
"exchange": {
"name": "bittrex",

View File

@ -132,6 +132,13 @@ You can run a one-off container that is immediately deleted upon exiting with th
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
There is known issue in OSX Docker versions after 17.09.1, whereby /etc/localtime cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd.
```bash
docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
More information on this docker issue and work-around can be read here: https://github.com/docker/for-mac/issues/2396
In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.

View File

@ -43,6 +43,10 @@ python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
```
Timerange doesn't work with live data.
To plot trades stored in a database use `--db-url` argument:
```
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH
```
## Plot profit

View File

@ -32,9 +32,12 @@ CREATE TABLE trades (
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT,
open_rate_requested FLOAT,
close_rate FLOAT,
close_rate_requested FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
@ -71,13 +74,13 @@ WHERE id=31;
```sql
INSERT
INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
```
**Example:**
```sql
INSERT INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
```
## Fix wrong fees in the table

View File

@ -42,7 +42,7 @@ Below, example of Telegram message you will receive for each command.
For each open trade, the bot will send you the following message.
> **Trade ID:** `123`
> **Current Pair:** BTC_CVC
> **Current Pair:** CVC/BTC
> **Open Since:** `1 days ago`
> **Amount:** `26.64180098`
> **Open Rate:** `0.00007489`
@ -57,8 +57,8 @@ Return the status of all open trades in a table format.
```
ID Pair Since Profit
---- -------- ------- --------
67 BTC_SC 1 d 13.33%
123 BTC_CVC 1 h 12.95%
67 SC/BTC 1 d 13.33%
123 CVC/BTC 1 h 12.95%
```
## /count
@ -83,7 +83,7 @@ Return a summary of your profit/loss and performance.
> **First Trade opened:** `3 days ago`
> **Latest Trade opened:** `2 minutes ago`
> **Avg. Duration:** `2:33:45`
> **Best Performing:** `BTC_PAY: 50.23%`
> **Best Performing:** `PAY/BTC: 50.23%`
## /forcesell <trade_id>
@ -92,11 +92,11 @@ Return a summary of your profit/loss and performance.
## /performance
Return the performance of each crypto-currency the bot has sold.
> Performance:
> 1. `BTC_RCN 57.77%`
> 2. `BTC_PAY 56.91%`
> 3. `BTC_VIB 47.07%`
> 4. `BTC_SALT 30.24%`
> 5. `BTC_STORJ 27.24%`
> 1. `RCN/BTC 57.77%`
> 2. `PAY/BTC 56.91%`
> 3. `VIB/BTC 47.07%`
> 4. `SALT/BTC 30.24%`
> 5. `STORJ/BTC 27.24%`
> ...
## /balance
@ -129,12 +129,8 @@ Day Profit BTC Profit USD
> **Version:** `0.14.3`
### using proxy with telegram
in [freqtrade/freqtrade/rpc/telegram.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/rpc/telegram.py) replace
```
self._updater = Updater(token=self._config['telegram']['token'], workers=0)
```
with
```
self._updater = Updater(token=self._config['telegram']['token'], request_kwargs={'proxy_url': 'socks5://127.0.0.1:1080/'}, workers=0)
$ export HTTP_PROXY="http://addr:port"
$ export HTTPS_PROXY="http://addr:port"
$ freqtrade
```

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """
__version__ = '0.16.0'
__version__ = '0.17.0'
class DependencyException(BaseException):
@ -16,9 +16,9 @@ class OperationalException(BaseException):
"""
class NotEnoughFundsExeption(BaseException):
class TemporaryError(BaseException):
"""
This happens when the exchange reports that not enough funds where available.
We do not want to stop the bot in this case and just keep it going and suppress
this message
Temporary network or exchange related error.
This could happen when an exchange is congested, unavailable, or the user
has networking problems. Usually resolves itself after a time.
"""

View File

@ -9,6 +9,7 @@ from typing import Dict, List, Tuple
import arrow
from pandas import DataFrame, to_datetime
from freqtrade import constants
from freqtrade.exchange import get_ticker_history
from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver
@ -45,19 +46,20 @@ class Analyze(object):
:param ticker: See exchange.get_ticker_history
:return: DataFrame
"""
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
frame = DataFrame(ticker).rename(columns=columns)
if 'BV' in frame:
frame.drop('BV', axis=1, inplace=True)
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
frame = DataFrame(ticker, columns=cols)
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
frame['date'] = to_datetime(frame['date'],
unit='ms',
utc=True,
infer_datetime_format=True)
# group by index and aggregate results to eliminate duplicate ticks
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
'close': 'last',
'open': 'first',
'high': 'max',
'low': 'min',
'open': 'first',
'close': 'last',
'volume': 'max',
})
return frame
@ -88,7 +90,7 @@ class Analyze(object):
"""
return self.strategy.populate_sell_trend(dataframe=dataframe)
def get_ticker_interval(self) -> int:
def get_ticker_interval(self) -> str:
"""
Return ticker interval to use
:return: Ticker interval value to use
@ -107,10 +109,10 @@ class Analyze(object):
dataframe = self.populate_sell_trend(dataframe)
return dataframe
def get_signal(self, pair: str, interval: int) -> Tuple[bool, bool]:
def get_signal(self, pair: str, interval: str) -> Tuple[bool, bool]:
"""
Calculates current signal based several technical analysis indicators
:param pair: pair in format BTC_ANT or BTC-ANT
:param pair: pair in format ANT/BTC
:param interval: Interval to use (in min)
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
"""
@ -144,7 +146,8 @@ class Analyze(object):
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
if signal_date < arrow.utcnow() - timedelta(minutes=(interval + 5)):
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < arrow.utcnow() - timedelta(minutes=(interval_minutes + 5)):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,

View File

@ -6,6 +6,7 @@ import argparse
import logging
import os
import re
import arrow
from typing import List, Tuple, Optional
from freqtrade import __version__, constants
@ -123,7 +124,7 @@ class Arguments(object):
)
parser.add_argument(
'-r', '--refresh-pairs-cached',
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
help='refresh the pairs files in tests/testdata with the latest data from the exchange. \
Use it if you want to run your backtesting with up-to-date data.',
action='store_true',
dest='refresh_pairs',
@ -141,10 +142,9 @@ class Arguments(object):
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval in minutes (1, 5, 30, 60, 1440)',
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
dest='ticker_interval',
type=int,
metavar='INT',
type=str,
)
parser.add_argument(
'--realistic-simulation',
@ -235,19 +235,23 @@ class Arguments(object):
stop = None
if stype[0]:
start = rvals[index]
if stype[0] != 'date':
if stype[0] == 'date':
start = arrow.get(start, 'YYYYMMDD').timestamp
else:
start = int(start)
index += 1
if stype[1]:
stop = rvals[index]
if stype[1] != 'date':
if stype[1] == 'date':
stop = arrow.get(stop, 'YYYYMMDD').timestamp
else:
stop = int(stop)
return stype, start, stop
raise Exception('Incorrect syntax for timerange "%s"' % text)
def scripts_options(self) -> None:
"""
Parses given arguments for plot scripts.
Parses given arguments for scripts.
"""
self.parser.add_argument(
'-p', '--pair',
@ -255,3 +259,41 @@ class Arguments(object):
dest='pair',
default=None
)
self.parser.add_argument(
'-db', '--db-url',
help='Show trades stored in database.',
dest='db_url',
default=None
)
def testdata_dl_options(self) -> None:
"""
Parses given arguments for testdata download
"""
self.parser.add_argument(
'--pairs-file',
help='File containing a list of pairs to download',
dest='pairs_file',
default=None
)
self.parser.add_argument(
'--export',
help='Export files to given dir',
dest='export',
default=None)
self.parser.add_argument(
'--days',
help='Download data for number of days',
dest='days',
type=int,
default=None)
self.parser.add_argument(
'--exchange',
help='Exchange name',
dest='exchange',
type=str,
default='bittrex')

View File

@ -6,11 +6,11 @@ import json
import logging
from argparse import Namespace
from typing import Dict, Any
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
import ccxt
from freqtrade import constants
from freqtrade import OperationalException, constants
logger = logging.getLogger(__name__)
@ -108,6 +108,9 @@ class Configuration(object):
else:
logger.info('Dry run is disabled. (--dry_run_db ignored)')
# Check if the exchange set by the user is supported
self.check_exchange(config)
return config
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
@ -121,7 +124,7 @@ class Configuration(object):
if 'ticker_interval' in self.args and self.args.ticker_interval:
config.update({'ticker_interval': self.args.ticker_interval})
logger.info('Parameter -i/--ticker-interval detected ...')
logger.info('Using ticker_interval: %d ...', config.get('ticker_interval'))
logger.info('Using ticker_interval: %s ...', config.get('ticker_interval'))
# If -l/--live is used we add it to the configuration
if 'live' in self.args and self.args.live:
@ -206,3 +209,23 @@ class Configuration(object):
self.config = self.load_config()
return self.config
def check_exchange(self, config: Dict[str, Any]) -> bool:
"""
Check if the exchange name in the config file is supported by Freqtrade
:return: True or raised an exception if the exchange if not supported
"""
exchange = config.get('exchange', {}).get('name').lower()
if exchange not in ccxt.exchanges:
exception_msg = 'Exchange "{}" not supported.\n' \
'The following exchanges are supported: {}'\
.format(exchange, ', '.join(ccxt.exchanges))
logger.critical(exception_msg)
raise OperationalException(
exception_msg
)
logger.debug('Exchange "%s" supported', exchange)
return True

View File

@ -10,12 +10,27 @@ HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
TICKER_INTERVAL_MINUTES = {
'1m': 1,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'12h': 720,
'1d': 1440,
'1w': 10080,
}
# Required json-schema for user specified config
CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 1},
'ticker_interval': {'type': 'integer', 'enum': [1, 5, 30, 60, 1440]},
'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'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',
@ -85,7 +100,7 @@ CONF_SCHEMA = {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
},
'uniqueItems': True
},
@ -93,7 +108,7 @@ CONF_SCHEMA = {
'type': 'array',
'items': {
'type': 'string',
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
},
'uniqueItems': True
}

View File

@ -1,33 +1,75 @@
# pragma pylint: disable=W0603
""" Cryptocurrency Exchanges support """
import enum
import logging
from random import randint
from typing import List, Dict, Any, Optional
from datetime import datetime
import ccxt
import arrow
import requests
from cachetools import cached, TTLCache
from freqtrade import OperationalException
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
logger = logging.getLogger(__name__)
# Current selected exchange
_API: Exchange = None
_CONF: dict = {}
_API: ccxt.Exchange = None
_CONF: Dict = {}
API_RETRY_COUNT = 4
# Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
# Urls to exchange markets, insert quote and base with .format()
_EXCHANGE_URLS = {
ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}',
ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}'
}
class Exchanges(enum.Enum):
def retrier(f):
def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT)
try:
return f(*args, **kwargs)
except (TemporaryError, DependencyException) as ex:
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
if count > 0:
count -= 1
kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count)
return wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
"""
Maps supported exchange names to correspondent classes.
Initialize ccxt with given config and return valid
ccxt instance.
:param config: config to use
:return: ccxt
"""
BITTREX = Bittrex
# Find matching class for the given exchange name
name = exchange_config['name']
if name not in ccxt.exchanges:
raise OperationalException('Exchange {} is not supported'.format(name))
try:
api = getattr(ccxt, name.lower())({
'apiKey': exchange_config.get('key'),
'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''),
'enableRateLimit': True,
})
except (KeyError, AttributeError):
raise OperationalException('Exchange {} is not supported'.format(name))
return api
def init(config: dict) -> None:
@ -46,15 +88,9 @@ def init(config: dict) -> None:
logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
_API = init_ccxt(exchange_config)
# Find matching class for the given exchange name
name = exchange_config['name']
try:
exchange_class = Exchanges[name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(name))
_API = exchange_class(exchange_config)
logger.info('Using Exchange "%s"', get_name())
# Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist'])
@ -67,119 +103,337 @@ def validate_pairs(pairs: List[str]) -> None:
:param pairs: list of pairs
:return: None
"""
try:
markets = _API.get_markets()
except requests.exceptions.RequestException as e:
markets = _API.load_markets()
except ccxt.BaseError as e:
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
return
stake_cur = _CONF['stake_currency']
for pair in pairs:
if not pair.startswith(stake_cur):
# Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
# TODO: add a support for having coins in BTC/USDT format
if not pair.endswith(stake_cur):
raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
)
if pair not in markets:
raise OperationalException(
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
'Pair {} is not available at {}'.format(pair, get_name()))
def buy(pair: str, rate: float, amount: float) -> str:
def exchange_has(endpoint: str) -> bool:
"""
Checks if exchange implements a specific API endpoint.
Wrapper around ccxt 'has' attribute
:param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
:return: bool
"""
return endpoint in _API.has and _API.has[endpoint]
def buy(pair: str, rate: float, amount: float) -> Dict:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'price': rate,
'amount': amount,
'type': 'LIMIT_BUY',
'type': 'limit',
'side': 'buy',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed',
'fee': None
}
return order_id
return {'id': order_id}
return _API.buy(pair, rate, amount)
try:
return _API.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
'Insufficient funds to create limit buy order on market {}.'
'Tried to buy amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not create limit buy order on market {}.'
'Tried to buy amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not place buy order due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
def sell(pair: str, rate: float, amount: float) -> str:
def sell(pair: str, rate: float, amount: float) -> Dict:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'price': rate,
'amount': amount,
'type': 'LIMIT_SELL',
'type': 'limit',
'side': 'sell',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
'datetime': arrow.utcnow().isoformat(),
'status': 'closed'
}
return order_id
return {'id': order_id}
return _API.sell(pair, rate, amount)
try:
return _API.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
'Insufficient funds to create limit sell order on market {}.'
'Tried to sell amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not create limit sell order on market {}.'
'Tried to sell amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not place sell order due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_balance(currency: str) -> float:
if _CONF['dry_run']:
return 999.9
return _API.get_balance(currency)
# ccxt exception is already handled by get_balances
balances = get_balances()
balance = balances.get(currency)
if balance is None:
raise TemporaryError(
'Could not get {} balance due to malformed exchange response: {}'.format(
currency, balances))
return balance['free']
def get_balances():
@retrier
def get_balances() -> dict:
if _CONF['dry_run']:
return []
return {}
return _API.get_balances()
try:
balances = _API.fetch_balance()
# Remove additional info from ccxt results
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get balance due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_tickers() -> Dict:
try:
return _API.fetch_tickers()
except ccxt.NotSupported as e:
raise OperationalException(
'Exchange {} does not support fetching tickers in batch.'
'Message: {}'.format(_API.name, e)
)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load tickers due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
# TODO: remove refresh argument, keeping it to keep track of where it was intended to be used
@retrier
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
return _API.get_ticker(pair, refresh)
try:
return _API.fetch_ticker(pair)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load ticker history due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
@cached(TTLCache(maxsize=100, ttl=30))
def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval)
@retrier
def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] = None) -> List[Dict]:
try:
# last item should be in the time interval [now - tick_interval, now]
till_time_ms = arrow.utcnow().shift(
minutes=-constants.TICKER_INTERVAL_MINUTES[tick_interval]
).timestamp * 1000
# it looks as if some exchanges return cached data
# and they update it one in several minute, so 10 mins interval
# is necessary to skeep downloading of an empty array when all
# chached data was already downloaded
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
data = []
while not since_ms or since_ms < till_time_ms:
data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
if not data_part:
break
logger.debug('Downloaded data for %s time range [%s, %s]',
pair,
arrow.get(data_part[0][0] / 1000).format(),
arrow.get(data_part[-1][0] / 1000).format())
data.extend(data_part)
since_ms = data[-1][0] + 1
return data
except ccxt.NotSupported as e:
raise OperationalException(
'Exchange {} does not support fetching historical candlestick data.'
'Message: {}'.format(_API.name, e)
)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load ticker history due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e))
def cancel_order(order_id: str) -> None:
@retrier
def cancel_order(order_id: str, pair: str) -> None:
if _CONF['dry_run']:
return
return _API.cancel_order(order_id)
try:
return _API.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not cancel order. Message: {}'.format(e)
)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not cancel order due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
def get_order(order_id: str) -> Dict:
@retrier
def get_order(order_id: str, pair: str) -> Dict:
if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({
'id': order_id
})
return order
try:
return _API.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not get order. Message: {}'.format(e)
)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get order due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
return _API.get_order(order_id)
@retrier
def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
if _CONF['dry_run']:
return []
if not exchange_has('fetchMyTrades'):
return []
try:
my_trades = _API.fetch_my_trades(pair, since.timestamp())
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
return matched_trades
except ccxt.NetworkError as e:
raise TemporaryError(
'Could not get trades due to networking error. Message: {}'.format(e)
)
except ccxt.BaseError as e:
raise OperationalException(e)
def get_pair_detail_url(pair: str) -> str:
return _API.get_pair_detail_url(pair)
try:
url_base = _API.urls.get('www')
base, quote = pair.split('/')
return url_base + _EXCHANGE_URLS[_API.id].format(base=base, quote=quote)
except KeyError:
logger.warning('Could not get exchange url for %s', get_name())
return ""
def get_markets() -> List[str]:
return _API.get_markets()
def get_market_summaries() -> List[Dict]:
return _API.get_market_summaries()
@retrier
def get_markets() -> List[dict]:
try:
return _API.fetch_markets()
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load markets due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
def get_name() -> str:
return _API.name
def get_fee() -> float:
return _API.fee
def get_id() -> str:
return _API.id
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()
@retrier
def get_fee(symbol='ETH/BTC', type='', side='', amount=1,
price=1, taker_or_maker='maker') -> float:
try:
# validate that markets are loaded before trying to get fee
if _API.markets is None or len(_API.markets) == 0:
_API.load_markets()
return _API.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get fee info due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
def get_amount_lots(pair: str, amount: float) -> float:
"""
get buyable amount rounding, ..
"""
# validate that markets are loaded before trying to get fee
if not _API.markets:
_API.load_markets()
return _API.amount_to_lots(pair, amount)

View File

@ -1,226 +0,0 @@
import logging
from typing import Dict, List, Optional
from bittrex.bittrex import API_V1_1, API_V2_0
from bittrex.bittrex import Bittrex as _Bittrex
from requests.exceptions import ContentDecodingError
from freqtrade import OperationalException, NotEnoughFundsExeption
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Bittrex = None
_API_V2: _Bittrex = None
_EXCHANGE_CONF: dict = {}
class Bittrex(Exchange):
"""
Bittrex API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
def __init__(self, config: dict) -> None:
global _API, _API_V2, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config)
_API = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V1_1,
)
_API_V2 = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V2_0,
)
self.cached_ticker = {}
@staticmethod
def _validate_response(response) -> None:
"""
Validates the given bittrex response
and raises a ContentDecodingError if a non-fatal issue happened.
"""
temp_error_messages = [
'NO_API_RESPONSE',
'MIN_TRADE_REQUIREMENT_NOT_MET',
]
if response['message'] in temp_error_messages:
raise ContentDecodingError(response['message'])
@property
def fee(self) -> float:
# 0.25 %: See https://bittrex.com/fees
return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
if data['message'] == "INSUFFICIENT_FUNDS":
raise NotEnoughFundsExeption('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
else:
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
Bittrex._validate_response(data)
if data['message'] == "INSUFFICIENT_FUNDS":
raise NotEnoughFundsExeption('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
else:
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid']
def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({currency})'.format(
message=data['message'],
currency=currency))
return float(data['result']['Balance'] or 0.0)
def get_balances(self):
data = _API.get_balances()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message}'.format(message=data['message']))
return data['result']
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
if refresh or pair not in self.cached_ticker.keys():
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
Bittrex._validate_response(data)
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 keys) or\
not all(data.get('result', {})[key] is not None for key in keys):
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
pair=pair))
# Update the pair
self.cached_ticker[pair] = {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
return self.cached_ticker[pair]
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
if tick_interval == 1:
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('Unknown tick_interval: {}'.format(tick_interval))
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
# These sanity check are necessary because bittrex cannot keep their API stable.
if not data.get('result'):
raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format(
pair=pair))
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']:
if prop not in tick.keys():
raise ContentDecodingError('Required property {} not present '
'in response params=({})'.format(prop, pair))
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
return data['result']
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
data = data['result']
return {
'id': data['OrderUuid'],
'type': data['Type'],
'pair': data['Exchange'].replace('-', '_'),
'opened': data['Opened'],
'rate': data['PricePerUnit'],
'amount': data['Quantity'],
'remaining': data['QuantityRemaining'],
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
data = _API.get_markets()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException(data['message'])
return [m['MarketName'].replace('-', '_') for m in data['result']]
def get_market_summaries(self) -> List[Dict]:
data = _API.get_market_summaries()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException(data['message'])
return data['result']
def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health()
if not data['success']:
Bittrex._validate_response(data)
raise OperationalException(data['message'])
return [{
'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'],
'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']]

View File

@ -1,172 +0,0 @@
from abc import ABC, abstractmethod
from typing import Dict, List, Optional
class Exchange(ABC):
@property
def name(self) -> str:
"""
Name of the exchange.
:return: str representation of the class name
"""
return self.__class__.__name__
@property
def fee(self) -> float:
"""
Fee for placing an order
:return: percentage in float
"""
@abstractmethod
def buy(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to purchase
:return: order_id of the placed buy order
"""
@abstractmethod
def sell(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to sell
:return: order_id of the placed sell order
"""
@abstractmethod
def get_balance(self, currency: str) -> float:
"""
Gets account balance.
:param currency: Currency as str, format: BTC
:return: float
"""
@abstractmethod
def get_balances(self) -> List[dict]:
"""
Gets account balances across currencies
:return: List of dicts, format: [
{
'Currency': str,
'Balance': float,
'Available': float,
'Pending': float,
}
...
]
"""
@abstractmethod
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
"""
Gets ticker for given pair.
:param pair: Pair as str, format: BTC_ETC
:param refresh: Shall we query a new value or a cached value is enough
:return: dict, format: {
'bid': float,
'ask': float,
'last': float
}
"""
@abstractmethod
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
"""
Gets ticker history for given pair.
:param pair: Pair as str, format: BTC_ETC
:param tick_interval: ticker interval in minutes
:return: list, format: [
{
'O': float, (Open)
'H': float, (High)
'L': float, (Low)
'C': float, (Close)
'V': float, (Volume)
'T': datetime, (Time)
'BV': float, (Base Volume)
},
...
]
"""
def get_order(self, order_id: str) -> Dict:
"""
Get order details for the given order_id.
:param order_id: ID as str
:return: dict, format: {
'id': str,
'type': str,
'pair': str,
'opened': str ISO 8601 datetime,
'closed': str ISO 8601 datetime,
'rate': float,
'amount': float,
'remaining': int
}
"""
@abstractmethod
def cancel_order(self, order_id: str) -> None:
"""
Cancels order for given order_id.
:param order_id: ID as str
:return: None
"""
@abstractmethod
def get_pair_detail_url(self, pair: str) -> str:
"""
Returns the market detail url for the given pair.
:param pair: Pair as str, format: BTC_ETC
:return: URL as str
"""
@abstractmethod
def get_markets(self) -> List[str]:
"""
Returns all available markets.
:return: List of all available pairs
"""
@abstractmethod
def get_market_summaries(self) -> List[Dict]:
"""
Returns a 24h market summary for all available markets
:return: list, format: [
{
'MarketName': str,
'High': float,
'Low': float,
'Volume': float,
'Last': float,
'TimeStamp': datetime,
'BaseVolume': float,
'Bid': float,
'Ask': float,
'OpenBuyOrders': int,
'OpenSellOrders': int,
'PrevDay': float,
'Created': datetime
},
...
]
"""
@abstractmethod
def get_wallet_health(self) -> List[Dict]:
"""
Returns a list of all wallet health information
:return: list, format: [
{
'Currency': str,
'IsActive': bool,
'LastChecked': str,
'Notice': str
},
...
"""

View File

@ -5,8 +5,10 @@ e.g BTC to USD
import logging
import time
from typing import Dict
from coinmarketcap import Market
from requests.exceptions import RequestException
logger = logging.getLogger(__name__)
@ -73,11 +75,7 @@ class CryptoToFiatConverter(object):
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
]
CRYPTOMAP = {
'BTC': 'bitcoin',
'ETH': 'ethereum',
'USDT': 'thether'
}
_cryptomap: Dict = {}
def __new__(cls):
if CryptoToFiatConverter.__instance is None:
@ -90,6 +88,15 @@ class CryptoToFiatConverter(object):
def __init__(self) -> None:
self._pairs = []
self._load_cryptomap()
def _load_cryptomap(self) -> None:
try:
coinlistings = self._coinmarketcap.listings()
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
coinlistings["data"]))
except (ValueError, RequestException) as e:
logger.error("Could not load FIAT Cryptocurrency map for the following problem: %s", e)
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
"""
@ -99,6 +106,8 @@ class CryptoToFiatConverter(object):
:param fiat_symbol: fiat to convert to
:return: float, value in fiat of the crypto-currency amount
"""
if crypto_symbol == fiat_symbol:
return crypto_amount
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
return float(crypto_amount) * float(price)
@ -179,15 +188,17 @@ class CryptoToFiatConverter(object):
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
if crypto_symbol not in self.CRYPTOMAP:
raise ValueError(
'The crypto symbol {} is not supported.'.format(crypto_symbol))
if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
return 0.0
try:
return float(
self._coinmarketcap.ticker(
currency=self.CRYPTOMAP[crypto_symbol],
currency=self._cryptomap[crypto_symbol],
convert=fiat_symbol
)[0]['price_' + fiat_symbol.lower()]
)['data']['quotes'][fiat_symbol.upper()]['price']
)
except BaseException:
except BaseException as ex:
logger.error("Error in _find_price: %s", ex)
return 0.0

View File

@ -3,7 +3,6 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade()
"""
import copy
import json
import logging
import time
import traceback
@ -12,13 +11,14 @@ from typing import Dict, List, Optional, Any, Callable
import arrow
import requests
from cachetools import cached, TTLCache
from cachetools import TTLCache, cached
from freqtrade import (
DependencyException, OperationalException, exchange, persistence, __version__,
NotEnoughFundsExeption)
from freqtrade.analyze import Analyze
DependencyException, OperationalException, TemporaryError,
exchange, persistence, __version__,
)
from freqtrade import constants
from freqtrade.analyze import Analyze
from freqtrade.fiat_convert import CryptoToFiatConverter
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc_manager import RPCManager
@ -165,78 +165,83 @@ class FreqtradeBot(object):
# Then looking for buy opportunities
if len(trades) < self.config['max_open_trades']:
try:
state_changed = self.process_maybe_execute_buy()
except NotEnoughFundsExeption:
logger.warning('insufficient funds to execute this buy order, ignoring it!')
state_changed = False
state_changed = self.process_maybe_execute_buy()
if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders
self.check_handle_timedout(self.config['unfilledtimeout'])
Trade.session.flush()
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
except TemporaryError as error:
logger.warning('%s, retrying in 30 seconds...', error)
time.sleep(constants.RETRY_TIMEOUT)
except OperationalException:
self.rpc.send_msg(
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
)
.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
)
)
logger.exception('OperationalException. Stopping trader ...')
self.state = State.STOPPED
return state_changed
@cached(TTLCache(maxsize=1, ttl=1800))
def _gen_pair_whitelist(self, base_currency: str, key: str = 'BaseVolume') -> List[str]:
def _gen_pair_whitelist(self, base_currency: str, key: str = 'quoteVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param key: sort key (defaults to 'BaseVolume')
:param key: sort key (defaults to 'quoteVolume')
:return: List of pairs
"""
summaries = sorted(
(s for s in exchange.get_market_summaries() if
s['MarketName'].startswith(base_currency)),
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries]
if not exchange.exchange_has('fetchTickers'):
raise OperationalException(
'Exchange does not support dynamic whitelist.'
'Please edit your config and restart the bot'
)
tickers = exchange.get_tickers()
# check length so that we make sure that '/' is actually in the string
tickers = [v for k, v in tickers.items()
if len(k.split('/')) == 2 and k.split('/')[1] == base_currency]
sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key])
pairs = [s['symbol'] for s in sorted_tickers]
return pairs
def _refresh_whitelist(self, whitelist: List[str]) -> List[str]:
"""
Check wallet health and remove pair from whitelist if necessary
Check available markets and remove pair from whitelist if necessary
:param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to
trade
:return: the list of pairs the user wants to trade without the one unavailable or
black_listed
"""
sanitized_whitelist = whitelist
health = exchange.get_wallet_health()
markets = exchange.get_markets()
markets = [m for m in markets if m['quote'] == self.config['stake_currency']]
known_pairs = set()
for status in health:
pair = '{}_{}'.format(self.config['stake_currency'], status['Currency'])
for market in markets:
pair = market['symbol']
# pair is not int the generated dynamic market, or in the blacklist ... ignore it
if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []):
continue
# else the pair is valid
known_pairs.add(pair)
# Market is not active
if not status['IsActive']:
if not market['active']:
sanitized_whitelist.remove(pair)
logger.info(
'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active'
'Ignoring %s from whitelist. Market is not active.',
pair
)
# We need to remove pairs that are unknown
final_list = [x for x in sanitized_whitelist if x in known_pairs]
return final_list
def get_target_bid(self, ticker: Dict[str, float]) -> float:
@ -281,7 +286,7 @@ class FreqtradeBot(object):
if not whitelist:
raise DependencyException('No currency pairs in whitelist')
# Pick pair based on StochRSI buy signals
# Pick pair based on buy signals
for _pair in whitelist:
(buy, sell) = self.analyze.get_signal(_pair, interval)
if buy and not sell:
@ -294,7 +299,7 @@ class FreqtradeBot(object):
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
order_id = exchange.buy(pair, buy_limit, amount)['id']
stake_amount_fiat = self.fiat_converter.convert_amount(
stake_amount,
@ -305,26 +310,29 @@ class FreqtradeBot(object):
# Create trade entity and return
self.rpc.send_msg(
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
.format(
exchange.get_name().upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit,
stake_amount,
self.config['stake_currency'],
stake_amount_fiat,
self.config['fiat_display_currency']
)
.format(
exchange.get_name(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit,
stake_amount,
self.config['stake_currency'],
stake_amount_fiat,
self.config['fiat_display_currency']
)
)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker')
trade = Trade(
pair=pair,
stake_amount=stake_amount,
amount=amount,
fee=exchange.get_fee(),
fee_open=fee,
fee_close=fee,
open_rate=buy_limit,
open_rate_requested=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
exchange=exchange.get_id(),
open_order_id=order_id
)
Trade.session.add(trade)
@ -352,17 +360,74 @@ class FreqtradeBot(object):
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('Found open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id))
try:
# Get order details for actual price per unit
if trade.open_order_id:
# Update trade with order values
logger.info('Found open order for %s', trade)
order = exchange.get_order(trade.open_order_id, trade.pair)
# Try update amount (binance-fix)
try:
new_amount = self.get_real_amount(trade, order)
if order['amount'] != new_amount:
order['amount'] = new_amount
# Fee was applied, so set to 0
trade.fee_open = 0
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return self.handle_trade(trade)
except OperationalException as exception:
logger.warning("could not update trade amount: %s", exception)
trade.update(order)
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair
return self.handle_trade(trade)
except DependencyException as exception:
logger.warning('Unable to sell trade: %s', exception)
return False
def get_real_amount(self, trade: Trade, order: Dict) -> float:
"""
Get real amount for the trade
Necessary for exchanges which charge fees in base currency (e.g. binance)
"""
order_amount = order['amount']
# Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open':
return order_amount
# use fee from order-dict if possible
if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}):
if trade.pair.startswith(order['fee']['currency']):
new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order",
trade, order['amount'], new_amount)
return new_amount
# Fallback to Trades
trades = exchange.get_trades_for_order(trade.open_order_id, trade.pair, trade.open_date)
if len(trades) == 0:
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
return order_amount
amount = 0
fee_abs = 0
for exectrade in trades:
amount += exectrade['amount']
if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}):
# only applies if fee is in quote currency!
if trade.pair.startswith(exectrade['fee']['currency']):
fee_abs += exectrade['fee']['cost']
if amount != order_amount:
logger.warning("amount {} does not match amount {}".format(amount, trade.amount))
raise OperationalException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0:
logger.info("Applying fee on amount for {} (from {} to {}) from Trades".format(
trade, order['amount'], real_amount))
return real_amount
def handle_trade(self, trade: Trade) -> bool:
"""
Sells the current pair if the threshold is reached and updates the trade record.
@ -395,22 +460,22 @@ class FreqtradeBot(object):
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try:
order = exchange.get_order(trade.open_order_id)
order = exchange.get_order(trade.open_order_id, trade.pair)
except requests.exceptions.RequestException:
logger.info(
'Cannot query order for %s due to %s',
trade,
traceback.format_exc())
continue
ordertime = arrow.get(order['opened'])
ordertime = arrow.get(order['datetime']).datetime
# Check if trade is still actually open
if int(order['remaining']) == 0:
continue
if order['type'] == "LIMIT_BUY" and ordertime < timeoutthreashold:
if order['side'] == 'buy' and ordertime < timeoutthreashold:
self.handle_timedout_limit_buy(trade, order)
elif order['type'] == "LIMIT_SELL" and ordertime < timeoutthreashold:
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
# FIX: 20180110, why is cancel.order unconditionally here, whereas
@ -420,7 +485,7 @@ class FreqtradeBot(object):
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
exchange.cancel_order(trade.open_order_id)
exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
@ -450,7 +515,7 @@ class FreqtradeBot(object):
"""
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
@ -472,64 +537,60 @@ class FreqtradeBot(object):
:return: None
"""
# Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id
trade.close_rate_requested = limit
# check if we have these funds first
try:
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id
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)['bid']
profit = trade.calc_profit_percent(limit)
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\n" \
"*Current Pair:* [{pair}]({pair_url})\n" \
"*Limit:* `{limit}`\n" \
"*Amount:* `{amount}`\n" \
"*Open Rate:* `{open_rate:.8f}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \
"*Profit:* `{profit:.2f}%`" \
"".format(
exchange=trade.exchange,
pair=trade.pair,
pair_url=exchange.get_pair_detail_url(trade.pair),
limit=limit,
open_rate=trade.open_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
profit=round(profit * 100, 2),
)
message = "*{exchange}:* Selling\n" \
"*Current Pair:* [{pair}]({pair_url})\n" \
"*Limit:* `{limit}`\n" \
"*Amount:* `{amount}`\n" \
"*Open Rate:* `{open_rate:.8f}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \
"*Profit:* `{profit:.2f}%`" \
"".format(
exchange=trade.exchange,
pair=trade.pair,
pair_url=exchange.get_pair_detail_url(trade.pair),
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
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
self.config['stake_currency'],
self.config['fiat_display_currency']
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`' \
''.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=self.config['stake_currency'],
profit_fiat=profit_fiat,
fiat=self.config['fiat_display_currency'],
)
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# For regular case, when the configuration exists
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
self.config['stake_currency'],
self.config['fiat_display_currency']
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`' \
''.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=self.config['stake_currency'],
profit_fiat=profit_fiat,
fiat=self.config['fiat_display_currency'],
)
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message
self.rpc.send_msg(message)
Trade.session.flush()
except NotEnoughFundsExeption:
logger.warning('Sell order failed, due to not having enough funds for %s.', trade)
# Send the message
self.rpc.send_msg(message)
Trade.session.flush()

View File

@ -61,6 +61,7 @@ def set_loggers() -> None:
:return: None
"""
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)

View File

@ -5,6 +5,7 @@ Various tool function for Freqtrade and scripts
import json
import logging
import re
import gzip
from datetime import datetime
from typing import Dict
@ -63,12 +64,28 @@ def common_datearray(dfs: Dict[str, DataFrame]) -> np.ndarray:
return np.sort(arr, axis=0)
def file_dump_json(filename, data) -> None:
def file_dump_json(filename, data, is_zip=False) -> None:
"""
Dump JSON data into a file
:param filename: file to create
:param data: JSON Data to save
:return:
"""
with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
print(f'dumping json to "{filename}"')
if is_zip:
if not filename.endswith('.gz'):
filename = filename + '.gz'
with gzip.open(filename, 'w') as fp:
json.dump(data, fp, default=str)
else:
with open(filename, 'w') as fp:
json.dump(data, fp, default=str)
def format_ms_time(date: str) -> str:
"""
convert MS date to readable format.
: epoch-string in ms
"""
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S')

View File

@ -4,38 +4,60 @@ import gzip
import json
import logging
import os
import arrow
from typing import Optional, List, Dict, Tuple
from freqtrade import misc
from freqtrade import misc, constants
from freqtrade.exchange import get_ticker_history
from user_data.hyperopt_conf import hyperopt_optimize_conf
logger = logging.getLogger(__name__)
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
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]
if not tickerlist:
return tickerlist
return tickerlist
stype, start, stop = timerange
start_index = 0
stop_index = len(tickerlist)
if stype[0] == 'line':
stop_index = start
if stype[0] == 'index':
start_index = start
elif stype[0] == 'date':
while start_index < len(tickerlist) and tickerlist[start_index][0] < start * 1000:
start_index += 1
if stype[1] == 'line':
start_index = len(tickerlist) + stop
if stype[1] == 'index':
stop_index = stop
elif stype[1] == 'date':
while stop_index > 0 and tickerlist[stop_index-1][0] > stop * 1000:
stop_index -= 1
if start_index > stop_index:
raise ValueError(f'The timerange [{start},{stop}] is incorrect')
return tickerlist[start_index:stop_index]
def load_tickerdata_file(
datadir: str, pair: str,
ticker_interval: int,
ticker_interval: str,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
"""
Load a pair from file,
:return dict OR empty if unsuccesful
"""
path = make_testdata_path(datadir)
pair_file_string = pair.replace('/', '_')
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format(
pair=pair,
pair=pair_file_string,
ticker_interval=ticker_interval,
))
gzipfile = file + '.gz'
@ -58,7 +80,8 @@ def load_tickerdata_file(
return pairdata
def load_data(datadir: str, ticker_interval: int,
def load_data(datadir: str,
ticker_interval: str,
pairs: Optional[List[str]] = None,
refresh_pairs: Optional[bool] = False,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
@ -73,16 +96,15 @@ def load_data(datadir: str, ticker_interval: int,
# 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, ticker_interval)
download_pairs(datadir, _pairs, ticker_interval, timerange=timerange)
for pair in _pairs:
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, timerange=timerange)
result[pair] = pairdata
if pairdata:
result[pair] = pairdata
else:
logger.warn('No data for pair %s, use --update-pairs-cached to download the data', pair)
return result
@ -95,14 +117,19 @@ def make_testdata_path(datadir: str) -> str:
)
def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
def download_pairs(datadir, pairs: List[str],
ticker_interval: str,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> bool:
"""For each pairs passed in parameters, download the ticker intervals"""
for pair in pairs:
try:
download_backtesting_testdata(datadir, pair=pair, interval=ticker_interval)
download_backtesting_testdata(datadir,
pair=pair,
tick_interval=ticker_interval,
timerange=timerange)
except BaseException:
logger.info(
'Failed to download the pair: "%s", Interval: %s min',
'Failed to download the pair: "%s", Interval: %s',
pair,
ticker_interval
)
@ -110,39 +137,85 @@ def download_pairs(datadir, pairs: List[str], ticker_interval: int) -> bool:
return True
# FIX: 20180110, suggest rename interval to tick_interval
def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> None:
def load_cached_data_for_updating(filename: str,
tick_interval: str,
timerange: Optional[Tuple[Tuple, int, int]]) -> Tuple[list, int]:
"""
Download the latest 1 and 5 ticker intervals from Bittrex for the pairs passed in parameters
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
Load cached data and choose what part of the data should be updated
"""
path = make_testdata_path(datadir)
logger.info(
'Download the pair: "%s", Interval: %s min', pair, interval
)
since_ms = None
filename = os.path.join(path, '{pair}-{interval}.json'.format(
pair=pair.replace("-", "_"),
interval=interval,
))
# user sets timerange, so find the start time
if timerange:
if timerange[0][0] == 'date':
since_ms = timerange[1] * 1000
elif timerange[0][1] == 'line':
num_minutes = timerange[2] * constants.TICKER_INTERVAL_MINUTES[tick_interval]
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
# read the cached file
if os.path.isfile(filename):
with open(filename, "rt") as file:
data = json.load(file)
# remove the last item, because we are not sure if it is correct
# it could be fetched when the candle was incompleted
if data:
data.pop()
else:
data = []
logger.debug('Current Start: %s', data[1]['T'] if data else None)
logger.debug('Current End: %s', data[-1:][0]['T'] if data else None)
if data:
if since_ms and since_ms < data[0][0]:
# the data is requested for earlier period than the cache has
# so fully redownload all the data
data = []
else:
# a part of the data was already downloaded, so
# download unexist data only
since_ms = data[-1][0] + 1
# Extend data with new ticker history
data.extend([
row for row in get_ticker_history(pair=pair, tick_interval=int(interval))
if row not in data
])
return (data, since_ms)
def download_backtesting_testdata(datadir: str,
pair: str,
tick_interval: str = '5m',
timerange: Optional[Tuple[Tuple, int, int]] = None) -> None:
"""
Download the latest ticker intervals from the exchange for the pairs passed in parameters
The data is downloaded starting from the last correct ticker interval data that
esists in a cache. If timerange starts earlier than the data in the cache,
the full data will be redownloaded
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
:param pairs: list of pairs to download
:param tick_interval: ticker interval
:param timerange: range of time to download
:return: None
"""
path = make_testdata_path(datadir)
filepair = pair.replace("/", "_")
filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
logger.info(
'Download the pair: "%s", Interval: %s',
pair,
tick_interval
)
data, since_ms = load_cached_data_for_updating(filename, tick_interval, timerange)
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
new_data = get_ticker_history(pair=pair, tick_interval=tick_interval, since_ms=since_ms)
data.extend(new_data)
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
logger.debug("New End: %s", misc.format_ms_time(data[-1][0]))
data = sorted(data, key=lambda _data: _data['T'])
logger.debug('New Start: %s', data[1]['T'])
logger.debug('New End: %s', data[-1:][0]['T'])
misc.file_dump_json(filename, data)

View File

@ -17,11 +17,9 @@ from freqtrade import exchange
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.exchange import Bittrex
from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
@ -52,7 +50,14 @@ class Backtesting(object):
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
self.populate_buy_trend = self.analyze.populate_buy_trend
self.populate_sell_trend = self.analyze.populate_sell_trend
exchange._API = Bittrex({'key': '', 'secret': ''})
# Reset keys for backtesting
self.config['exchange']['key'] = ''
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
exchange.init(self.config)
@staticmethod
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
@ -109,12 +114,14 @@ class Backtesting(object):
stake_amount = args['stake_amount']
max_open_trades = args.get('max_open_trades', 0)
fee = exchange.get_fee()
trade = Trade(
open_rate=buy_row.close,
open_date=buy_row.date,
stake_amount=stake_amount,
amount=stake_amount / buy_row.open,
fee=exchange.get_fee()
fee_open=fee,
fee_close=fee
)
# calculate win/lose forwards from buy point

View File

@ -29,7 +29,6 @@ from freqtrade.optimize import load_data
from freqtrade.optimize.backtesting import Backtesting
from user_data.hyperopt_conf import hyperopt_optimize_conf
logger = logging.getLogger(__name__)

View File

@ -15,6 +15,8 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
from sqlalchemy import inspect
logger = logging.getLogger(__name__)
@ -50,12 +52,61 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
Trade.session = session()
Trade.query = session.query_property()
_DECL_BASE.metadata.create_all(engine)
check_migrate(engine)
# Clean dry_run DB
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
clean_dry_run_db()
def has_column(columns, searchname: str) -> bool:
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
def check_migrate(engine) -> None:
"""
Checks if migration is necessary and migrates if necessary
"""
inspector = inspect(engine)
cols = inspector.get_columns('trades')
if not has_column(cols, 'fee_open'):
# Schema migration necessary
engine.execute("alter table trades rename to trades_bak")
# let SQLAlchemy create the schema as required
_DECL_BASE.metadata.create_all(engine)
# Copy data back - following the correct schema
engine.execute("""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id)
select id, lower(exchange),
case
when instr(pair, '_') != 0 then
substr(pair, instr(pair, '_') + 1) || '/' ||
substr(pair, 1, instr(pair, '_') - 1)
else pair
end
pair,
is_open, fee fee_open, fee fee_close,
open_rate, null open_rate_requested, close_rate,
null close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id
from trades_bak
""")
# Reread columns - the above recreated the table!
inspector = inspect(engine)
cols = inspector.get_columns('trades')
if not has_column(cols, 'open_rate_requested'):
engine.execute("alter table trades add open_rate_requested float")
if not has_column(cols, 'close_rate_requested'):
engine.execute("alter table trades add close_rate_requested float")
def cleanup() -> None:
"""
Flushes all pending operations to disk.
@ -85,9 +136,12 @@ class Trade(_DECL_BASE):
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
fee = Column(Float, nullable=False, default=0.0)
fee_open = Column(Float, nullable=False, default=0.0)
fee_close = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float)
open_rate_requested = Column(Float)
close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float)
stake_amount = Column(Float, nullable=False)
amount = Column(Float)
@ -111,20 +165,20 @@ class Trade(_DECL_BASE):
:return: None
"""
# Ignore open and cancelled orders
if not order['closed'] or order['rate'] is None:
if order['status'] == 'open' or order['price'] is None:
return
logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'LIMIT_BUY':
if order['type'] == 'limit' and order['side'] == 'buy':
# Update open rate and actual amount
self.open_rate = Decimal(order['rate'])
self.open_rate = Decimal(order['price'])
self.amount = Decimal(order['amount'])
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
self.open_order_id = None
elif order['type'] == 'LIMIT_SELL':
self.close(order['rate'])
elif order['type'] == 'limit' and order['side'] == 'sell':
self.close(order['price'])
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
cleanup()
@ -156,7 +210,7 @@ class Trade(_DECL_BASE):
getcontext().prec = 8
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee)
fees = buy_trade * Decimal(fee or self.fee_open)
return float(buy_trade + fees)
def calc_close_trade_price(
@ -177,7 +231,7 @@ class Trade(_DECL_BASE):
return 0.0
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
fees = sell_trade * Decimal(fee or self.fee)
fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees)
def calc_profit(
@ -195,7 +249,7 @@ class Trade(_DECL_BASE):
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=(rate or self.close_rate),
fee=(fee or self.fee)
fee=(fee or self.fee_close)
)
return float("{0:.8f}".format(close_trade_price - open_trade_price))
@ -215,7 +269,7 @@ class Trade(_DECL_BASE):
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=(rate or self.close_rate),
fee=(fee or self.fee)
fee=(fee or self.fee_close)
)
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))

View File

@ -48,7 +48,7 @@ class RPC(object):
for trade in trades:
order = None
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
order = exchange.get_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate)
@ -76,8 +76,8 @@ class RPC(object):
amount=round(trade.amount, 8),
close_profit=fmt_close_profit,
current_profit=round(current_profit * 100, 2),
open_order='({} rem={:.8f})'.format(
order['type'], order['remaining']
open_order='({} {} rem={:.8f})'.format(
order['type'], order['side'], order['remaining']
) if order else None,
)
result.append(message)
@ -246,35 +246,34 @@ class RPC(object):
"""
:return: current account balance per crypto
"""
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
return True, '`All balances are zero.`'
output = []
total = 0.0
for currency in balances:
coin = currency['Currency']
for coin, balance in exchange.get_balances().items():
if not balance['total']:
continue
rate = None
if coin == 'BTC':
currency["Rate"] = 1.0
rate = 1.0
else:
if coin == 'USDT':
currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid']
rate = 1.0 / exchange.get_ticker('BTC/USDT', False)['bid']
else:
currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid']
currency['BTC'] = currency["Rate"] * currency["Balance"]
total = total + currency['BTC']
rate = exchange.get_ticker(coin + '/BTC', False)['bid']
est_btc: float = rate * balance['total']
total = total + est_btc
output.append(
{
'currency': currency['Currency'],
'available': currency['Available'],
'balance': currency['Balance'],
'pending': currency['Pending'],
'est_btc': currency['BTC']
'currency': coin,
'available': balance['free'],
'balance': balance['total'],
'pending': balance['used'],
'est_btc': est_btc
}
)
if total == 0.0:
return True, '`All balances are zero.`'
fiat = self.freqtrade.fiat_converter
symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol)
@ -310,17 +309,21 @@ class RPC(object):
def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
order = exchange.get_order(trade.open_order_id, trade.pair)
# Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id)
trade.close(order.get('rate') or trade.open_rate)
if order and order['status'] == 'open' \
and order['type'] == 'limit' \
and order['side'] == 'buy':
exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close(order.get('price') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Ignore trades with an attached LIMIT_SELL order
if order and not order['closed'] and order['type'] == 'LIMIT_SELL':
if order and order['status'] == 'open' \
and order['type'] == 'limit' \
and order['side'] == 'sell':
return
# Get current rate and execute sell

View File

@ -264,17 +264,15 @@ class Telegram(RPC):
(currencys, total, symbol, value) = result
output = ''
for currency in currencys:
output += """*Currency*: {currency}
*Available*: {available}
*Balance*: {balance}
*Pending*: {pending}
*Est. BTC*: {est_btc: .8f}
""".format(**currency)
output += "*{currency}:*\n" \
"\t`Available: {available: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \
"\t`Pending: {pending: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
output += """*Estimated Value*:
*BTC*: {0: .8f}
*{1}*: {2: .2f}
""".format(total, symbol, value)
output += "\n*Estimated Value*:\n" \
"\t`BTC: {0: .8f}`\n" \
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
self.send_msg(output)
@authorized_only

View File

@ -26,7 +26,7 @@ class DefaultStrategy(IStrategy):
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = 5
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""

View File

@ -59,7 +59,6 @@ class StrategyResolver(object):
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss)
self.strategy.ticker_interval = int(self.strategy.ticker_interval)
def _load_strategy(
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:

View File

@ -2,6 +2,7 @@
import json
import logging
from datetime import datetime
from typing import Dict, Optional
from functools import reduce
from unittest.mock import MagicMock
@ -34,7 +35,8 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
:param config: Config to pass to the bot
:return: None
"""
mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
# mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@ -46,6 +48,27 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
return FreqtradeBot(config, create_engine('sqlite://'))
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
"""
Mocker to coinmarketcap to speed up tests
:param mocker: mocker to patch coinmarketcap class
:return: None
"""
tickermock = MagicMock(return_value={'price_usd': 12345.0})
listmock = MagicMock(return_value={'data': [{'id': 1, 'name': 'Bitcoin', 'symbol': 'BTC',
'website_slug': 'bitcoin'},
{'id': 1027, 'name': 'Ethereum', 'symbol': 'ETH',
'website_slug': 'ethereum'}
]})
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=tickermock,
listings=listmock,
)
@pytest.fixture(scope="function")
def default_conf():
""" Returns validated configuration suitable for most tests """
@ -54,7 +77,7 @@ def default_conf():
"stake_currency": "BTC",
"stake_amount": 0.001,
"fiat_display_currency": "USD",
"ticker_interval": 5,
"ticker_interval": '5m',
"dry_run": True,
"minimal_roi": {
"40": 0.0,
@ -73,11 +96,10 @@ def default_conf():
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT",
"BTC_BCC"
"ETH/BTC",
"LTC/BTC",
"XRP/BTC",
"NEO/BTC"
]
},
"telegram": {
@ -99,6 +121,11 @@ def update():
return _update
@pytest.fixture
def fee():
return MagicMock(return_value=0.0025)
@pytest.fixture
def ticker():
return MagicMock(return_value={
@ -127,46 +154,94 @@ def ticker_sell_down():
@pytest.fixture
def health():
return MagicMock(return_value=[{
'Currency': 'BTC',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'ETH',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'TRST',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'SWT',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'BCC',
'IsActive': False,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}])
def markets():
return MagicMock(return_value=[
{
'id': 'ethbtc',
'symbol': 'ETH/BTC',
'base': 'ETH',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': 500000,
},
'info': '',
},
{
'id': 'tknbtc',
'symbol': 'TKN/BTC',
'base': 'TKN',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': 500000,
},
'info': '',
},
{
'id': 'blkbtc',
'symbol': 'BLK/BTC',
'base': 'BLK',
'quote': 'BTC',
'active': True,
'precision': {
'price': 8,
'amount': 8,
'cost': 8,
},
'lot': 0.00000001,
'limits': {
'amount': {
'min': 0.01,
'max': 1000,
},
'price': 500000,
'cost': 500000,
},
'info': '',
}
])
@pytest.fixture
def markets_empty():
return MagicMock(return_value=[])
@pytest.fixture(scope='function')
def limit_buy_order():
return {
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001099,
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 0.0,
'closed': str(arrow.utcnow().datetime),
'status': 'closed'
}
@ -174,12 +249,14 @@ def limit_buy_order():
def limit_buy_order_old():
return {
'id': 'mocked_limit_buy_old',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
'status': 'open'
}
@ -187,12 +264,14 @@ def limit_buy_order_old():
def limit_sell_order_old():
return {
'id': 'mocked_limit_sell_old',
'type': 'LIMIT_SELL',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'sell',
'pair': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 90.99181073,
'status': 'open'
}
@ -200,12 +279,14 @@ def limit_sell_order_old():
def limit_buy_order_old_partial():
return {
'id': 'mocked_limit_buy_old_partial',
'type': 'LIMIT_BUY',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'type': 'limit',
'side': 'buy',
'pair': 'ETH/BTC',
'datetime': arrow.utcnow().shift(minutes=-601).isoformat(),
'price': 0.00001099,
'amount': 90.99181073,
'remaining': 67.99181073,
'status': 'open'
}
@ -213,86 +294,228 @@ def limit_buy_order_old_partial():
def limit_sell_order():
return {
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'type': 'limit',
'side': 'sell',
'pair': 'mocked',
'opened': str(arrow.utcnow().datetime),
'rate': 0.00001173,
'datetime': arrow.utcnow().isoformat(),
'price': 0.00001173,
'amount': 90.99181073,
'remaining': 0.0,
'closed': str(arrow.utcnow().datetime),
'status': 'closed'
}
@pytest.fixture
def ticker_history():
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",
"BV": 0.0877869
},
{
"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",
"BV": 0.05874751
},
{
"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",
"BV": 0.7039405
}
[
1511686200000, # unix timestamp ms
8.794e-05, # open
8.948e-05, # high
8.794e-05, # low
8.88e-05, # close
0.0877869, # volume (in quote currency)
],
[
1511686500000,
8.88e-05,
8.942e-05,
8.88e-05,
8.893e-05,
0.05874751,
],
[
1511686800000,
8.891e-05,
8.893e-05,
8.875e-05,
8.877e-05,
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"
def tickers():
return MagicMock(return_value={
'ETH/BTC': {
'symbol': 'ETH/BTC',
'timestamp': 1522014806207,
'datetime': '2018-03-25T21:53:26.207Z',
'high': 0.061697,
'low': 0.060531,
'bid': 0.061588,
'bidVolume': 3.321,
'ask': 0.061655,
'askVolume': 0.212,
'vwap': 0.06105296,
'open': 0.060809,
'close': 0.060761,
'first': None,
'last': 0.061588,
'change': 1.281,
'percentage': None,
'average': None,
'baseVolume': 111649.001,
'quoteVolume': 6816.50176926,
'info': {}
},
{
"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"
'TKN/BTC': {
'symbol': 'TKN/BTC',
'timestamp': 1522014806169,
'datetime': '2018-03-25T21:53:26.169Z',
'high': 0.01885,
'low': 0.018497,
'bid': 0.018799,
'bidVolume': 8.38,
'ask': 0.018802,
'askVolume': 15.0,
'vwap': 0.01869197,
'open': 0.018585,
'close': 0.018573,
'baseVolume': 81058.66,
'quoteVolume': 2247.48374509,
},
{
"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"
'BLK/BTC': {
'symbol': 'BLK/BTC',
'timestamp': 1522014806072,
'datetime': '2018-03-25T21:53:26.720Z',
'high': 0.007745,
'low': 0.007512,
'bid': 0.007729,
'bidVolume': 0.01,
'ask': 0.007743,
'askVolume': 21.37,
'vwap': 0.00761466,
'open': 0.007653,
'close': 0.007652,
'first': None,
'last': 0.007743,
'change': 1.176,
'percentage': None,
'average': None,
'baseVolume': 295152.26,
'quoteVolume': 1515.14631229,
'info': {}
},
'LTC/BTC': {
'symbol': 'LTC/BTC',
'timestamp': 1523787258992,
'datetime': '2018-04-15T10:14:19.992Z',
'high': 0.015978,
'low': 0.0157,
'bid': 0.015954,
'bidVolume': 12.83,
'ask': 0.015957,
'askVolume': 0.49,
'vwap': 0.01581636,
'open': 0.015823,
'close': 0.01582,
'first': None,
'last': 0.015951,
'change': 0.809,
'percentage': None,
'average': None,
'baseVolume': 88620.68,
'quoteVolume': 1401.65697943,
'info': {}
},
'ETH/USDT': {
'symbol': 'ETH/USDT',
'timestamp': 1522014804118,
'datetime': '2018-03-25T21:53:24.118Z',
'high': 530.88,
'low': 512.0,
'bid': 529.73,
'bidVolume': 0.2,
'ask': 530.21,
'askVolume': 0.2464,
'vwap': 521.02438405,
'open': 527.27,
'close': 528.42,
'first': None,
'last': 530.21,
'change': 0.558,
'percentage': None,
'average': None,
'baseVolume': 72300.0659,
'quoteVolume': 37670097.3022171,
'info': {}
},
'TKN/USDT': {
'symbol': 'TKN/USDT',
'timestamp': 1522014806198,
'datetime': '2018-03-25T21:53:26.198Z',
'high': 8718.0,
'low': 8365.77,
'bid': 8603.64,
'bidVolume': 0.15846,
'ask': 8603.67,
'askVolume': 0.069147,
'vwap': 8536.35621697,
'open': 8680.0,
'close': 8680.0,
'first': None,
'last': 8603.67,
'change': -0.879,
'percentage': None,
'average': None,
'baseVolume': 30414.604298,
'quoteVolume': 259629896.48584127,
'info': {}
},
'BLK/USDT': {
'symbol': 'BLK/USDT',
'timestamp': 1522014806145,
'datetime': '2018-03-25T21:53:26.145Z',
'high': 66.95,
'low': 63.38,
'bid': 66.473,
'bidVolume': 4.968,
'ask': 66.54,
'askVolume': 2.704,
'vwap': 65.0526901,
'open': 66.43,
'close': 66.383,
'first': None,
'last': 66.5,
'change': 0.105,
'percentage': None,
'average': None,
'baseVolume': 294106.204,
'quoteVolume': 19132399.743954,
'info': {}
},
'LTC/USDT': {
'symbol': 'LTC/USDT',
'timestamp': 1523787257812,
'datetime': '2018-04-15T10:14:18.812Z',
'high': 129.94,
'low': 124.0,
'bid': 129.28,
'bidVolume': 0.03201,
'ask': 129.52,
'askVolume': 0.14529,
'vwap': 126.92838682,
'open': 127.0,
'close': 127.1,
'first': None,
'last': 129.28,
'change': 1.795,
'percentage': None,
'average': None,
'baseVolume': 59698.79897,
'quoteVolume': 29132399.743954,
'info': {}
}
]
})
# FIX: Perhaps change result fixture to use BTC_UNITEST instead?
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
return Analyze.parse_ticker_dataframe(json.load(data_file))
# FIX:
# Create an fixture/function
# that inserts a trade of some type and open-status
@ -300,132 +523,88 @@ def result():
# See tests in rpc/main that could use this
@pytest.fixture(scope="function")
def trades_for_order():
return [{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 8.0,
'fee': {'cost': 0.008, 'currency': 'LTC'}}]
@pytest.fixture(scope="function")
def trades_for_order2():
return [{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 4.0,
'fee': {'cost': 0.004, 'currency': 'LTC'}},
{'info': {'id': 34567,
'orderId': 123456,
'price': '0.24544100',
'qty': '8.00000000',
'commission': '0.00800000',
'commissionAsset': 'LTC',
'time': 1521663363189,
'isBuyer': True,
'isMaker': False,
'isBestMatch': True},
'timestamp': 1521663363189,
'datetime': '2018-03-21T20:16:03.189Z',
'symbol': 'LTC/ETH',
'id': '34567',
'order': '123456',
'type': None,
'side': 'buy',
'price': 0.245441,
'cost': 1.963528,
'amount': 4.0,
'fee': {'cost': 0.004, 'currency': 'LTC'}}]
@pytest.fixture
def get_market_summaries_data():
"""
This fixture is a real result from exchange.get_market_summaries() but reduced to only
8 entries. 4 BTC, 4 USTD
:return: JSON market summaries
"""
return [
{
'Ask': 1.316e-05,
'BaseVolume': 5.72599471,
'Bid': 1.3e-05,
'Created': '2014-04-14T00:00:00',
'High': 1.414e-05,
'Last': 1.298e-05,
'Low': 1.282e-05,
'MarketName': 'BTC-XWC',
'OpenBuyOrders': 2000,
'OpenSellOrders': 1484,
'PrevDay': 1.376e-05,
'TimeStamp': '2018-02-05T01:32:40.493',
'Volume': 424041.21418375
},
{
'Ask': 0.00627051,
'BaseVolume': 93.23302388,
'Bid': 0.00618192,
'Created': '2016-10-20T04:48:30.387',
'High': 0.00669897,
'Last': 0.00618192,
'Low': 0.006,
'MarketName': 'BTC-XZC',
'OpenBuyOrders': 343,
'OpenSellOrders': 2037,
'PrevDay': 0.00668229,
'TimeStamp': '2018-02-05T01:32:43.383',
'Volume': 14863.60730702
},
{
'Ask': 0.01137247,
'BaseVolume': 383.55922657,
'Bid': 0.01136006,
'Created': '2016-11-15T20:29:59.73',
'High': 0.012,
'Last': 0.01137247,
'Low': 0.01119883,
'MarketName': 'BTC-ZCL',
'OpenBuyOrders': 1332,
'OpenSellOrders': 5317,
'PrevDay': 0.01179603,
'TimeStamp': '2018-02-05T01:32:42.773',
'Volume': 33308.07358285
},
{
'Ask': 0.04155821,
'BaseVolume': 274.75369074,
'Bid': 0.04130002,
'Created': '2016-10-28T17:13:10.833',
'High': 0.04354429,
'Last': 0.041585,
'Low': 0.0413,
'MarketName': 'BTC-ZEC',
'OpenBuyOrders': 863,
'OpenSellOrders': 5579,
'PrevDay': 0.0429,
'TimeStamp': '2018-02-05T01:32:43.21',
'Volume': 6479.84033259
},
{
'Ask': 210.99999999,
'BaseVolume': 615132.70989532,
'Bid': 210.05503736,
'Created': '2017-07-21T01:08:49.397',
'High': 257.396,
'Last': 211.0,
'Low': 209.05333589,
'MarketName': 'USDT-XMR',
'OpenBuyOrders': 180,
'OpenSellOrders': 1203,
'PrevDay': 247.93528899,
'TimeStamp': '2018-02-05T01:32:43.117',
'Volume': 2688.17410793
},
{
'Ask': 0.79589979,
'BaseVolume': 9349557.01853031,
'Bid': 0.789226,
'Created': '2017-07-14T17:10:10.737',
'High': 0.977,
'Last': 0.79589979,
'Low': 0.781,
'MarketName': 'USDT-XRP',
'OpenBuyOrders': 1075,
'OpenSellOrders': 6508,
'PrevDay': 0.93300218,
'TimeStamp': '2018-02-05T01:32:42.383',
'Volume': 10801663.00788851
},
{
'Ask': 0.05154982,
'BaseVolume': 2311087.71232136,
'Bid': 0.05040107,
'Created': '2017-12-29T19:29:18.357',
'High': 0.06668561,
'Last': 0.0508,
'Low': 0.05006731,
'MarketName': 'USDT-XVG',
'OpenBuyOrders': 655,
'OpenSellOrders': 5544,
'PrevDay': 0.0627,
'TimeStamp': '2018-02-05T01:32:41.507',
'Volume': 40031424.2152716
},
{
'Ask': 332.65500022,
'BaseVolume': 562911.87455665,
'Bid': 330.00000001,
'Created': '2017-07-14T17:10:10.673',
'High': 401.59999999,
'Last': 332.65500019,
'Low': 330.0,
'MarketName': 'USDT-ZEC',
'OpenBuyOrders': 161,
'OpenSellOrders': 1731,
'PrevDay': 391.42,
'TimeStamp': '2018-02-05T01:32:42.947',
'Volume': 1571.09647946
}
]
def buy_order_fee():
return {
'id': 'mocked_limit_buy_old',
'type': 'limit',
'side': 'buy',
'pair': 'mocked',
'datetime': str(arrow.utcnow().shift(minutes=-601).datetime),
'price': 0.245441,
'amount': 8.0,
'remaining': 90.99181073,
'status': 'closed',
'fee': None
}

View File

@ -1,16 +1,18 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access
import logging
from copy import deepcopy
from random import randint
from unittest.mock import MagicMock
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest
from requests.exceptions import RequestException
import freqtrade.exchange as exchange
from freqtrade import OperationalException
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
get_ticker, get_ticker_history, cancel_order, get_name, get_fee
from freqtrade import OperationalException, DependencyException, TemporaryError
from freqtrade.exchange import (init, validate_pairs, buy, sell, get_balance, get_balances,
get_ticker, get_ticker_history, cancel_order, get_name, get_fee,
get_id, get_pair_detail_url, get_amount_lots)
from freqtrade.tests.conftest import log_has
API_INIT = False
@ -42,9 +44,12 @@ def test_init_exception(default_conf):
def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
])
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
})
id_mock = PropertyMock(return_value='test_exchange')
type(api_mock).id = id_mock
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
validate_pairs(default_conf['exchange']['pair_whitelist'])
@ -52,7 +57,7 @@ def test_validate_pairs(default_conf, mocker):
def test_validate_pairs_not_available(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=[])
api_mock.load_markets = MagicMock(return_value={})
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(OperationalException, match=r'not available'):
@ -61,63 +66,148 @@ def test_validate_pairs_not_available(default_conf, mocker):
def test_validate_pairs_not_compatible(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_markets = MagicMock(
return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT'])
default_conf['stake_currency'] = 'ETH'
api_mock.load_markets = MagicMock(return_value={
'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': ''
})
conf = deepcopy(default_conf)
conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
mocker.patch.dict('freqtrade.exchange._CONF', conf)
with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
validate_pairs(conf['exchange']['pair_whitelist'])
def test_validate_pairs_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
api_mock = MagicMock()
api_mock.get_markets = MagicMock(side_effect=RequestException())
api_mock.name = 'Binance'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
api_mock.load_markets = MagicMock(return_value={})
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
validate_pairs(default_conf['exchange']['pair_whitelist'])
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
validate_pairs(default_conf['exchange']['pair_whitelist'])
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
caplog.record_tuples)
def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
conf = deepcopy(default_conf)
conf['stake_currency'] = 'ETH'
api_mock = MagicMock()
api_mock.name = 'binance'
mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', conf)
with pytest.raises(
OperationalException,
match=r'Pair ETH/BTC not compatible with stake_currency: ETH'
):
validate_pairs(default_conf['exchange']['pair_whitelist'])
def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
order = buy(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'dry_run_buy_' in order['id']
def test_buy_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.buy = MagicMock(
return_value='dry_run_buy_{}'.format(randint(0, 10**6)))
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_limit_buy_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_buy_' in buy(pair='BTC_ETH', rate=200, amount=1)
order = buy(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InsufficientFunds)
mocker.patch('freqtrade.exchange._API', api_mock)
buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(DependencyException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.InvalidOrder)
mocker.patch('freqtrade.exchange._API', api_mock)
buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(TemporaryError):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
buy(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(OperationalException):
api_mock.create_limit_buy_order = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
buy(pair='ETH/BTC', rate=200, amount=1)
def test_sell_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
order = sell(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'dry_run_sell_' in order['id']
def test_sell_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.sell = MagicMock(
return_value='dry_run_sell_{}'.format(randint(0, 10**6)))
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
api_mock.create_limit_sell_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert 'dry_run_sell_' in sell(pair='BTC_ETH', rate=200, amount=1)
order = sell(pair='ETH/BTC', rate=200, amount=1)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InsufficientFunds)
mocker.patch('freqtrade.exchange._API', api_mock)
sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(DependencyException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.InvalidOrder)
mocker.patch('freqtrade.exchange._API', api_mock)
sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(TemporaryError):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
sell(pair='ETH/BTC', rate=200, amount=1)
with pytest.raises(OperationalException):
api_mock.create_limit_sell_order = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
sell(pair='ETH/BTC', rate=200, amount=1)
def test_get_balance_dry_run(default_conf, mocker):
@ -129,7 +219,7 @@ def test_get_balance_dry_run(default_conf, mocker):
def test_get_balance_prod(default_conf, mocker):
api_mock = MagicMock()
api_mock.get_balance = MagicMock(return_value=123.4)
api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}})
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
@ -137,36 +227,53 @@ def test_get_balance_prod(default_conf, mocker):
assert get_balance(currency='BTC') == 123.4
with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
get_balance(currency='BTC')
def test_get_balances_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert get_balances() == []
assert get_balances() == {}
def test_get_balances_prod(default_conf, mocker):
balance_item = {
'Currency': '1ST',
'Balance': 10.0,
'Available': 10.0,
'Pending': 0.0,
'CryptoAddress': None
'free': 10.0,
'total': 10.0,
'used': 0.0
}
api_mock = MagicMock()
api_mock.get_balances = MagicMock(
return_value=[balance_item, balance_item, balance_item])
api_mock.fetch_balance = MagicMock(return_value={
'1ST': balance_item,
'2ST': balance_item,
'3ST': balance_item
})
mocker.patch('freqtrade.exchange._API', api_mock)
default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert len(get_balances()) == 3
assert get_balances()[0]['Currency'] == '1ST'
assert get_balances()[0]['Balance'] == 10.0
assert get_balances()[0]['Available'] == 10.0
assert get_balances()[0]['Pending'] == 0.0
assert get_balances()['1ST']['free'] == 10.0
assert get_balances()['1ST']['total'] == 10.0
assert get_balances()['1ST']['used'] == 0.0
with pytest.raises(TemporaryError):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
get_balances()
assert api_mock.fetch_balance.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
get_balances()
assert api_mock.fetch_balance.call_count == 1
# This test is somewhat redundant with
@ -174,57 +281,123 @@ def test_get_balances_prod(default_conf, mocker):
def test_get_ticker(default_conf, mocker):
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)
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
tick = {
'symbol': 'ETH/BTC',
'bid': 0.00001098,
'ask': 0.00001099,
'last': 0.0001,
}
api_mock.fetch_ticker = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange._API', api_mock)
# retrieve original ticker
ticker = get_ticker(pair='BTC_ETH')
ticker = get_ticker(pair='ETH/BTC')
assert ticker['bid'] == 0.00001098
assert ticker['ask'] == 0.00001099
# change the ticker
tick = {"success": True, 'result': {"Bid": 0.5, "Ask": 1, "Last": 42}}
api_mock.get_ticker = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange.bittrex._API', api_mock)
tick = {
'symbol': 'ETH/BTC',
'bid': 0.5,
'ask': 1,
'last': 42,
}
api_mock.fetch_ticker = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange._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
ticker = get_ticker(pair='ETH/BTC')
# force ticker refresh
ticker = get_ticker(pair='BTC_ETH', refresh=True)
assert ticker['bid'] == 0.5
assert ticker['ask'] == 1
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
get_ticker(pair='ETH/BTC', refresh=True)
with pytest.raises(OperationalException):
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
get_ticker(pair='ETH/BTC', refresh=True)
def make_fetch_ohlcv_mock(data):
def fetch_ohlcv_mock(pair, timeframe, since):
if since:
assert since > data[-1][0]
return []
return data
return fetch_ohlcv_mock
def test_get_ticker_history(default_conf, mocker):
api_mock = MagicMock()
tick = 123
api_mock.get_ticker_history = MagicMock(return_value=tick)
tick = [
[
1511686200000, # unix timestamp ms
1, # open
2, # high
3, # low
4, # close
5, # volume (in quote currency)
]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(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
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686200000
assert ticks[0][1] == 1
assert ticks[0][2] == 2
assert ticks[0][3] == 3
assert ticks[0][4] == 4
assert ticks[0][5] == 5
# change the ticker
tick = 999
api_mock.get_ticker_history = MagicMock(return_value=tick)
# change ticker and ensure tick changes
new_tick = [
[
1511686210000, # unix timestamp ms
6, # open
7, # high
8, # low
9, # close
10, # volume (in quote currency)
]
]
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_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
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1511686210000
assert ticks[0][1] == 6
assert ticks[0][2] == 7
assert ticks[0][3] == 8
assert ticks[0][4] == 9
assert ticks[0][5] == 10
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
# new symbol to get around cache
get_ticker_history('ABCD/BTC', default_conf['ticker_interval'])
with pytest.raises(OperationalException):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
# new symbol to get around cache
get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert cancel_order(order_id='123') is None
assert cancel_order(order_id='123', pair='TKN/BTC') is None
# Ensure that if not dry_run, we should call API
@ -234,7 +407,25 @@ def test_cancel_order(default_conf, mocker):
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123)
mocker.patch('freqtrade.exchange._API', api_mock)
assert cancel_order(order_id='_') == 123
assert cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(TemporaryError):
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(DependencyException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
mocker.patch('freqtrade.exchange._API', api_mock)
cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1
def test_get_order(default_conf, mocker):
@ -243,44 +434,93 @@ def test_get_order(default_conf, mocker):
order = MagicMock()
order.myid = 123
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
print(exchange.get_order('X'))
assert exchange.get_order('X').myid == 123
print(exchange.get_order('X', 'TKN/BTC'))
assert exchange.get_order('X', 'TKN/BTC').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)
api_mock.fetch_order = MagicMock(return_value=456)
mocker.patch('freqtrade.exchange._API', api_mock)
assert exchange.get_order('X') == 456
assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(TemporaryError):
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(DependencyException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == exchange.API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError)
mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
def test_get_name(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance'
init(default_conf)
assert get_name() == 'Binance'
def test_get_id(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance'
init(default_conf)
assert get_id() == 'binance'
def test_get_pair_detail_url(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
default_conf['exchange']['name'] = 'binance'
init(default_conf)
url = get_pair_detail_url('TKN/ETH')
assert 'TKN' in url
assert 'ETH' in url
url = get_pair_detail_url('LOOONG/BTC')
assert 'LOOONG' in url
assert 'BTC' in url
default_conf['exchange']['name'] = 'bittrex'
init(default_conf)
assert get_name() == 'Bittrex'
url = get_pair_detail_url('TKN/ETH')
assert 'TKN' in url
assert 'ETH' in url
url = get_pair_detail_url('LOOONG/BTC')
assert 'LOOONG' in url
assert 'BTC' in url
def test_get_fee(default_conf, mocker):
mocker.patch('freqtrade.exchange.validate_pairs',
side_effect=lambda s: True)
init(default_conf)
assert get_fee() == 0.0025
def test_exchange_misc(mocker):
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(return_value={
'type': 'taker',
'currency': 'BTC',
'rate': 0.025,
'cost': 0.05
})
mocker.patch('freqtrade.exchange._API', api_mock)
exchange.get_markets()
assert api_mock.get_markets.call_count == 1
exchange.get_market_summaries()
assert api_mock.get_market_summaries.call_count == 1
api_mock.name = 123
assert exchange.get_name() == 123
api_mock.fee = 456
assert exchange.get_fee() == 456
exchange.get_wallet_health()
assert api_mock.get_wallet_health.call_count == 1
assert get_fee() == 0.025
def test_get_amount_lots(default_conf, mocker):
api_mock = MagicMock()
api_mock.amount_to_lots = MagicMock(return_value=1.0)
mocker.patch('freqtrade.exchange._API', api_mock)
assert get_amount_lots('LTC/BTC', 1.54) == 1

View File

@ -1,349 +0,0 @@
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
from unittest.mock import MagicMock
import pytest
from requests.exceptions import ContentDecodingError
import freqtrade.exchange.bittrex as btx
from freqtrade.exchange.bittrex import Bittrex
# Eat this flake8
# +------------------+
# | bittrex.Bittrex |
# +------------------+
# |
# (mock Fake_bittrex)
# |
# +-----------------------------+
# | freqtrade.exchange.Bittrex |
# +-----------------------------+
# Call into Bittrex will flow up to the
# external package bittrex.Bittrex.
# By inserting a mock, we redirect those
# calls.
# The faked bittrex API is called just 'fb'
# The freqtrade.exchange.Bittrex is a
# wrapper, and is called 'wb'
def _stub_config():
return {'key': '',
'secret': ''}
class FakeBittrex():
def __init__(self, success=True):
self.success = True # Believe in yourself
self.result = None
self.get_ticker_call_count = 0
# This is really ugly, doing side-effect during instance creation
# But we're allowed to in testing-code
btx._API = MagicMock()
btx._API.buy_limit = self.fake_buysell_limit
btx._API.sell_limit = self.fake_buysell_limit
btx._API.get_balance = self.fake_get_balance
btx._API.get_balances = self.fake_get_balances
btx._API.get_ticker = self.fake_get_ticker
btx._API.get_order = self.fake_get_order
btx._API.cancel = self.fake_cancel_order
btx._API.get_markets = self.fake_get_markets
btx._API.get_market_summaries = self.fake_get_market_summaries
btx._API_V2 = MagicMock()
btx._API_V2.get_candles = self.fake_get_candles
btx._API_V2.get_wallet_health = self.fake_get_wallet_health
def fake_buysell_limit(self, pair, amount, limit):
return {'success': self.success,
'result': {'uuid': '1234'},
'message': 'barter'}
def fake_get_balance(self, cur):
return {'success': self.success,
'result': {'Balance': 1234},
'message': 'unbalanced'}
def fake_get_balances(self):
return {'success': self.success,
'result': [{'BTC_ETH': 1234}],
'message': 'no balances'}
def fake_get_ticker(self, pair):
self.get_ticker_call_count += 1
return self.result or {'success': self.success,
'result': {'Bid': 1, 'Ask': 1, 'Last': 1},
'message': 'NO_API_RESPONSE'}
def fake_get_candles(self, pair, interval):
return self.result or {'success': self.success,
'result': [{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}],
'message': 'candles lit'}
def fake_get_order(self, uuid):
return {'success': self.success,
'result': {'OrderUuid': 'ABC123',
'Type': 'Type',
'Exchange': 'BTC_ETH',
'Opened': True,
'PricePerUnit': 1,
'Quantity': 1,
'QuantityRemaining': 1,
'Closed': True},
'message': 'lost'}
def fake_cancel_order(self, uuid):
return self.result or {'success': self.success,
'message': 'no such order'}
def fake_get_markets(self):
return self.result or {'success': self.success,
'message': 'market gone',
'result': [{'MarketName': '-_'}]}
def fake_get_market_summaries(self):
return self.result or {'success': self.success,
'message': 'no summary',
'result': ['sum']}
def fake_get_wallet_health(self):
return self.result or {'success': self.success,
'message': 'bad health',
'result': [{'Health': {'Currency': 'BTC_ETH',
'IsActive': True,
'LastChecked': 0},
'Currency': {'Notice': True}}]}
# The freqtrade.exchange.bittrex is called wrap_bittrex
# to not confuse naming with bittrex.bittrex
def make_wrap_bittrex():
conf = _stub_config()
wb = btx.Bittrex(conf)
return wb
def test_exchange_bittrex_class():
conf = _stub_config()
b = Bittrex(conf)
assert isinstance(b, Bittrex)
slots = dir(b)
for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances',
'get_ticker', 'get_ticker_history', 'get_order',
'cancel_order', 'get_pair_detail_url', 'get_markets',
'get_market_summaries', 'get_wallet_health']:
assert name in slots
# FIX: ensure that the slot is also a method in the class
# getattr(b, name) => bound method Bittrex.buy
# type(getattr(b, name)) => class 'method'
def test_exchange_bittrex_fee():
fee = Bittrex.fee.__get__(Bittrex)
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
def test_exchange_bittrex_buy_good():
wb = make_wrap_bittrex()
fb = FakeBittrex()
uuid = wb.buy('BTC_ETH', 1, 1)
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'barter.*'):
wb.buy('BAD', 1, 1)
def test_exchange_bittrex_sell_good():
wb = make_wrap_bittrex()
fb = FakeBittrex()
uuid = wb.sell('BTC_ETH', 1, 1)
assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'barter.*'):
uuid = wb.sell('BAD', 1, 1)
def test_exchange_bittrex_get_balance():
wb = make_wrap_bittrex()
fb = FakeBittrex()
bal = wb.get_balance('BTC_ETH')
assert bal == fb.fake_get_balance(1)['result']['Balance']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'unbalanced'):
wb.get_balance('BTC_ETH')
def test_exchange_bittrex_get_balances():
wb = make_wrap_bittrex()
fb = FakeBittrex()
bals = wb.get_balances()
assert bals == fb.fake_get_balances()['result']
fb.success = False
with pytest.raises(btx.OperationalException, match=r'no balances'):
wb.get_balances()
def test_exchange_bittrex_get_ticker():
wb = make_wrap_bittrex()
fb = FakeBittrex()
# Poll ticker, which updates the cache
tick = wb.get_ticker('BTC_ETH')
for x in ['bid', 'ask', 'last']:
assert x in tick
# Ensure the side-effect was made (update the ticker cache)
assert 'BTC_ETH' in wb.cached_ticker.keys()
# taint the cache, so we can recognize the cache wall utilized
wb.cached_ticker['BTC_ETH']['bid'] = 1234
# Poll again, getting the cached result
fb.get_ticker_call_count = 0
tick = wb.get_ticker('BTC_ETH', False)
# Ensure the result was from the cache, and that we didn't call exchange
assert wb.cached_ticker['BTC_ETH']['bid'] == 1234
assert fb.get_ticker_call_count == 0
def test_exchange_bittrex_get_ticker_bad():
wb = make_wrap_bittrex()
fb = FakeBittrex()
fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': False, 'message': 'gone bad'}
with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': True, 'result': {}} # incomplete result
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'):
wb.get_ticker('BTC_ETH')
fb.result = {'success': False, 'message': 'gone 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'.*Invalid response from Bittrex params.*'):
wb.get_ticker('BTC_ETH')
def test_exchange_bittrex_get_ticker_history_intervals():
wb = make_wrap_bittrex()
FakeBittrex()
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():
wb = make_wrap_bittrex()
fb = FakeBittrex()
assert wb.get_ticker_history('BTC_ETH', 5)
with pytest.raises(ValueError, match=r'.*Unknown tick_interval.*'):
wb.get_ticker_history('BTC_ETH', 2)
fb.success = False
with pytest.raises(btx.OperationalException, match=r'candles lit.*'):
wb.get_ticker_history('BTC_ETH', 5)
fb.success = True
with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex.*'):
fb.result = {'bad': 0}
wb.get_ticker_history('BTC_ETH', 5)
with pytest.raises(ContentDecodingError, match=r'.*Required property C not present.*'):
fb.result = {'success': True,
'result': [{'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], # close is missing
'message': 'candles lit'}
wb.get_ticker_history('BTC_ETH', 5)
def test_exchange_bittrex_get_order():
wb = make_wrap_bittrex()
fb = FakeBittrex()
order = wb.get_order('someUUID')
assert order['id'] == 'ABC123'
fb.success = False
with pytest.raises(btx.OperationalException, match=r'lost'):
wb.get_order('someUUID')
def test_exchange_bittrex_cancel_order():
wb = make_wrap_bittrex()
fb = FakeBittrex()
wb.cancel_order('someUUID')
with pytest.raises(btx.OperationalException, match=r'no such order'):
fb.success = False
wb.cancel_order('someUUID')
# Note: this can be a bug in exchange.bittrex._validate_response
with pytest.raises(KeyError):
fb.result = {'success': False} # message is missing!
wb.cancel_order('someUUID')
with pytest.raises(btx.OperationalException, match=r'foo'):
fb.result = {'success': False, 'message': 'foo'}
wb.cancel_order('someUUID')
def test_exchange_get_pair_detail_url():
wb = make_wrap_bittrex()
assert wb.get_pair_detail_url('BTC_ETH')
def test_exchange_get_markets():
wb = make_wrap_bittrex()
fb = FakeBittrex()
x = wb.get_markets()
assert x == ['__']
with pytest.raises(btx.OperationalException, match=r'market gone'):
fb.success = False
wb.get_markets()
def test_exchange_get_market_summaries():
wb = make_wrap_bittrex()
fb = FakeBittrex()
assert ['sum'] == wb.get_market_summaries()
with pytest.raises(btx.OperationalException, match=r'no summary'):
fb.success = False
wb.get_market_summaries()
def test_exchange_get_wallet_health():
wb = make_wrap_bittrex()
fb = FakeBittrex()
x = wb.get_wallet_health()
assert x[0]['Currency'] == 'BTC_ETH'
with pytest.raises(btx.OperationalException, match=r'bad health'):
fb.success = False
wb.get_wallet_health()
def test_validate_response_success():
response = {
'message': '',
'result': [],
}
Bittrex._validate_response(response)
def test_validate_response_no_api_response():
response = {
'message': 'NO_API_RESPONSE',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'):
Bittrex._validate_response(response)
def test_validate_response_min_trade_requirement_not_met():
response = {
'message': 'MIN_TRADE_REQUIREMENT_NOT_MET',
'result': None,
}
with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'):
Bittrex._validate_response(response)

View File

@ -15,10 +15,7 @@ from freqtrade import optimize
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
from freqtrade.tests.conftest import default_conf, log_has
# Avoid to reinit the same object again and again
_BACKTESTING = Backtesting(default_conf())
from freqtrade.tests.conftest import log_has
def get_args(args) -> List[str]:
@ -34,49 +31,60 @@ def trim_dictlist(dict_list, num):
def load_data_test(what):
timerange = ((None, 'line'), None, -100)
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'], timerange=timerange)
pair = data['BTC_UNITEST']
data = optimize.load_data(None, ticker_interval='1m',
pairs=['UNITTEST/BTC'], timerange=timerange)
pair = data['UNITTEST/BTC']
datalen = len(pair)
# Depending on the what parameter we now adjust the
# loaded data looks:
# pair :: [{'O': 0.123, 'H': 0.123, 'L': 0.123,
# 'C': 0.123, 'V': 123.123,
# 'T': '2017-11-04T23:02:00', 'BV': 0.123}]
# pair :: [[ 1509836520000, unix timestamp in ms
# 0.00162008, open
# 0.00162008, high
# 0.00162008, low
# 0.00162008, close
# 108.14853839 base volume
# ]]
base = 0.001
if what == 'raise':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': x * base, # But replace O,H,L,C
'H': x * base + 0.0001,
'L': x * base - 0.0001,
'C': x * base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
x * base, # But replace O,H,L,C
x * base + 0.0001,
x * base - 0.0001,
x * base,
pair[x][5], # Keep old volume
] for x in range(0, datalen)
]}
if what == 'lower':
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
'O': 1 - x * base, # But replace O,H,L,C
'H': 1 - x * base + 0.0001,
'L': 1 - x * base - 0.0001,
'C': 1 - x * base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
1 - x * base, # But replace O,H,L,C
1 - x * base + 0.0001,
1 - x * base - 0.0001,
1 - x * base,
pair[x][5] # Keep old volume
] for x in range(0, datalen)
]}
if what == 'sine':
hz = 0.1 # frequency
return {'BTC_UNITEST':
[{'T': pair[x]['T'], # Keep old dates
'V': pair[x]['V'], # Keep old volume
'BV': pair[x]['BV'], # keep too
# But replace O,H,L,C
'O': math.sin(x * hz) / 1000 + base,
'H': math.sin(x * hz) / 1000 + base + 0.0001,
'L': math.sin(x * hz) / 1000 + base - 0.0001,
'C': math.sin(x * hz) / 1000 + base} for x in range(0, datalen)]}
return {'UNITTEST/BTC': [
[
pair[x][0], # Keep old dates
math.sin(x * hz) / 1000 + base, # But replace O,H,L,C
math.sin(x * hz) / 1000 + base + 0.0001,
math.sin(x * hz) / 1000 + base - 0.0001,
math.sin(x * hz) / 1000 + base,
pair[x][5] # Keep old volume
] for x in range(0, datalen)
]}
return data
def simple_backtest(config, contour, num_results) -> None:
backtesting = _BACKTESTING
def simple_backtest(config, contour, num_results, mocker) -> None:
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(config)
data = load_data_test(contour)
processed = backtesting.tickerdata_to_dataframe(data)
@ -93,9 +101,9 @@ def simple_backtest(config, contour, num_results) -> None:
assert len(results) == num_results
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}
def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=False, timerange=None):
tickerdata = optimize.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange)
pairdata = {'UNITTEST/BTC': tickerdata}
return pairdata
@ -107,12 +115,14 @@ def _load_pair_as_ticks(pair, tickfreq):
# FIX: fixturize this?
def _make_backtest_conf(conf=None, pair='BTC_UNITEST', record=None):
data = optimize.load_data(None, ticker_interval=8, pairs=[pair])
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
data = trim_dictlist(data, -200)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(conf)
return {
'stake_amount': conf['stake_amount'],
'processed': _BACKTESTING.tickerdata_to_dataframe(data),
'processed': backtesting.tickerdata_to_dataframe(data),
'max_open_trades': 10,
'realistic': True,
'record': record
@ -148,21 +158,6 @@ def _trend_alternate(dataframe=None):
return dataframe
def _run_backtest_1(fun, backtest_conf):
# strategy is a global (hidden as a singleton), so we
# emulate strategy being pure, by override/restore here
# if we dont do this, the override in strategy will carry over
# to other tests
old_buy = _BACKTESTING.populate_buy_trend
old_sell = _BACKTESTING.populate_sell_trend
_BACKTESTING.populate_buy_trend = fun # Override
_BACKTESTING.populate_sell_trend = fun # Override
results = _BACKTESTING.backtest(backtest_conf)
_BACKTESTING.populate_buy_trend = old_buy # restore override
_BACKTESTING.populate_sell_trend = old_sell # restore override
return results
# Unit tests
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
"""
@ -218,7 +213,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
@ -240,18 +235,18 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert log_has(
'Using ticker_interval: 1 ...',
'Using ticker_interval: 1m ...',
caplog.record_tuples
)
assert 'live' in config
assert log_has('Parameter -l/--live detected ...', caplog.record_tuples)
assert 'realistic_simulation'in config
assert 'realistic_simulation' in config
assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples)
assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples)
assert 'refresh_pairs'in config
assert 'refresh_pairs' in config
assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples)
assert 'timerange' in config
assert log_has(
@ -266,11 +261,13 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
)
def test_start(mocker, default_conf, caplog) -> None:
def test_start(mocker, fee, default_conf, caplog) -> None:
"""
Test start() function
"""
start_mock = MagicMock()
mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.backtesting.Backtesting.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
@ -306,49 +303,51 @@ def test_backtesting__init__(mocker, default_conf) -> None:
assert init_mock.call_count == 1
def test_backtesting_init(default_conf) -> None:
def test_backtesting_init(mocker, default_conf) -> None:
"""
Test Backtesting._init() method
"""
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert isinstance(backtesting.analyze, Analyze)
assert backtesting.ticker_interval == 5
assert backtesting.ticker_interval == '5m'
assert callable(backtesting.tickerdata_to_dataframe)
assert callable(backtesting.populate_buy_trend)
assert callable(backtesting.populate_sell_trend)
def test_tickerdata_to_dataframe(default_conf) -> None:
def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
"""
Test Backtesting.tickerdata_to_dataframe() method
"""
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
timerange = ((None, 'line'), None, -100)
tick = optimize.load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
backtesting = _BACKTESTING
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100
assert len(data['UNITTEST/BTC']) == 100
# Load Analyze to compare the result between Backtesting function and Analyze are the same
analyze = Analyze(default_conf)
data2 = analyze.tickerdata_to_dataframe(tickerlist)
assert data['BTC_UNITEST'].equals(data2['BTC_UNITEST'])
assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC'])
def test_get_timeframe() -> None:
def test_get_timeframe(default_conf, mocker) -> None:
"""
Test Backtesting.get_timeframe() method
"""
backtesting = _BACKTESTING
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(
optimize.load_data(
None,
ticker_interval=1,
pairs=['BTC_UNITEST']
ticker_interval='1m',
pairs=['UNITTEST/BTC']
)
)
min_date, max_date = backtesting.get_timeframe(data)
@ -356,15 +355,16 @@ def test_get_timeframe() -> None:
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
def test_generate_text_table():
def test_generate_text_table(default_conf, mocker):
"""
Test Backtesting.generate_text_table() method
"""
backtesting = _BACKTESTING
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
results = pd.DataFrame(
{
'currency': ['BTC_ETH', 'BTC_ETH'],
'currency': ['ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2],
'profit_BTC': [0.2, 0.4],
'duration': [10, 30],
@ -378,25 +378,27 @@ def test_generate_text_table():
'total profit BTC avg duration profit loss\n'
'------- ----------- -------------- '
'------------------ -------------- -------- ------\n'
'BTC_ETH 2 15.00 '
'ETH/BTC 2 15.00 '
'0.60000000 20.0 2 0\n'
'TOTAL 2 15.00 '
'0.60000000 20.0 2 0'
)
assert backtesting._generate_text_table(data={'BTC_ETH': {}}, results=results) == result_str
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
def test_backtesting_start(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method
"""
def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', mocked_load_data)
mocker.patch('freqtrade.exchange.get_ticker_history')
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
@ -405,7 +407,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['ticker_interval'] = 1
conf['live'] = False
conf['datadir'] = None
@ -426,13 +428,15 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert log_has(line, caplog.record_tuples)
def test_backtest(default_conf) -> None:
def test_backtest(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method
"""
backtesting = _BACKTESTING
mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
@ -445,14 +449,16 @@ def test_backtest(default_conf) -> None:
assert not results.empty
def test_backtest_1min_ticker_interval(default_conf) -> None:
def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method with 1 min ticker
"""
backtesting = _BACKTESTING
mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
# Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
data = trim_dictlist(data, -200)
results = backtesting.backtest(
{
@ -465,15 +471,16 @@ def test_backtest_1min_ticker_interval(default_conf) -> None:
assert not results.empty
def test_processed() -> None:
def test_processed(default_conf, mocker) -> None:
"""
Test Backtesting.backtest() method with offline data
"""
backtesting = _BACKTESTING
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(default_conf)
dict_of_tickerrows = load_data_test('raise')
dataframes = backtesting.tickerdata_to_dataframe(dict_of_tickerrows)
dataframe = dataframes['BTC_UNITEST']
dataframe = dataframes['UNITTEST/BTC']
cols = dataframe.columns
# assert the dataframe got some of the indicator columns
for col in ['close', 'high', 'low', 'open', 'date',
@ -481,76 +488,101 @@ def test_processed() -> None:
assert col in cols
def test_backtest_pricecontours(default_conf) -> None:
def test_backtest_pricecontours(default_conf, fee, mocker) -> None:
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
for [contour, numres] in tests:
simple_backtest(default_conf, contour, numres)
simple_backtest(default_conf, contour, numres, mocker)
# Test backtest using offline data (testdata directory)
def test_backtest_ticks(default_conf):
def test_backtest_ticks(default_conf, fee, mocker):
mocker.patch('freqtrade.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
ticks = [1, 5]
fun = _BACKTESTING.populate_buy_trend
for tick in ticks:
backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(fun, backtest_conf)
fun = Backtesting(default_conf).populate_buy_trend
for _ in ticks:
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert not results.empty
def test_backtest_clash_buy_sell(default_conf):
# Override the default buy trend function in our DefaultStrategy
def test_backtest_clash_buy_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy
def fun(dataframe=None):
buy_value = 1
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(fun, backtest_conf)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty
def test_backtest_only_sell(default_conf):
# Override the default buy trend function in our DefaultStrategy
def test_backtest_only_sell(mocker, default_conf):
# Override the default buy trend function in our default_strategy
def fun(dataframe=None):
buy_value = 0
sell_value = 1
return _trend(dataframe, buy_value, sell_value)
backtest_conf = _make_backtest_conf(conf=default_conf)
results = _run_backtest_1(fun, backtest_conf)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtest_conf = _make_backtest_conf(mocker, conf=default_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = fun # Override
backtesting.populate_sell_trend = fun # Override
results = backtesting.backtest(backtest_conf)
assert results.empty
def test_backtest_alternate_buy_sell(default_conf):
backtest_conf = _make_backtest_conf(conf=default_conf, pair='BTC_UNITEST')
results = _run_backtest_1(_trend_alternate, backtest_conf)
def test_backtest_alternate_buy_sell(default_conf, fee, mocker):
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC')
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override
results = backtesting.backtest(backtest_conf)
assert len(results) == 3
def test_backtest_record(default_conf, mocker):
def test_backtest_record(default_conf, fee, mocker):
names = []
records = []
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
mocker.patch(
'freqtrade.optimize.backtesting.file_dump_json',
new=lambda n, r: (names.append(n), records.append(r))
)
backtest_conf = _make_backtest_conf(
mocker,
conf=default_conf,
pair='BTC_UNITEST',
pair='UNITTEST/BTC',
record="trades"
)
results = _run_backtest_1(_trend_alternate, backtest_conf)
backtesting = Backtesting(default_conf)
backtesting.populate_buy_trend = _trend_alternate # Override
backtesting.populate_sell_trend = _trend_alternate # Override
results = backtesting.backtest(backtest_conf)
assert len(results) == 3
# Assert file_dump_json was only called once
assert names == ['backtest-result.json']
records = records[0]
# Ensure records are of correct type
assert len(records) == 3
# ('BTC_UNITEST', 0.00331158, '1510684320', '1510691700', 0, 117)
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records
oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur) in records:
assert pair == 'BTC_UNITEST'
assert pair == 'UNITTEST/BTC'
isinstance(profit, float)
# FIX: buy/sell should be converted to ints
isinstance(date_buy, str)
@ -563,13 +595,15 @@ def test_backtest_record(default_conf, mocker):
def test_backtest_start_live(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
mocker.patch('freqtrade.exchange.get_ticker_history',
new=lambda n, i: _load_pair_as_ticks(n, i))
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
read_data=json.dumps(conf)
))
args = MagicMock()
@ -585,7 +619,7 @@ def test_backtest_start_live(default_conf, mocker, caplog):
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--live',
'--timerange', '-100'
]
@ -594,7 +628,7 @@ def test_backtest_start_live(default_conf, mocker, caplog):
# check the logs, that will contain the backtest result
exists = [
'Parameter -i/--ticker-interval detected ...',
'Using ticker_interval: 1 ...',
'Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Using max_open_trades: 1 ...',
'Parameter --timerange detected: -100 ..',

View File

@ -1,20 +1,33 @@
# pragma pylint: disable=missing-docstring,W0212,C0103
import json
import os
import signal
from copy import deepcopy
from unittest.mock import MagicMock
import pandas as pd
import pytest
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.optimize.hyperopt import Hyperopt, start
from freqtrade.strategy.resolver import StrategyResolver
from freqtrade.tests.conftest import default_conf, log_has
from freqtrade.tests.conftest import log_has
from freqtrade.tests.optimize.test_backtesting import get_args
# Avoid to reinit the same object again and again
_HYPEROPT = Hyperopt(default_conf())
_HYPEROPT_INITIALIZED = False
_HYPEROPT = None
@pytest.fixture(scope='function')
def init_hyperopt(default_conf, mocker):
global _HYPEROPT_INITIALIZED, _HYPEROPT
if not _HYPEROPT_INITIALIZED:
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf',
MagicMock(return_value=default_conf))
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
_HYPEROPT = Hyperopt(default_conf)
_HYPEROPT_INITIALIZED = True
# Functions for recurrent object patching
@ -51,9 +64,10 @@ def test_start(mocker, default_conf, caplog) -> None:
"""
start_mock = MagicMock()
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(default_conf)
))
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf',
MagicMock(return_value=default_conf))
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
@ -74,7 +88,7 @@ def test_start(mocker, default_conf, caplog) -> None:
assert start_mock.call_count == 1
def test_loss_calculation_prefer_correct_trade_count() -> None:
def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None:
"""
Test Hyperopt.calculate_loss()
"""
@ -88,7 +102,7 @@ def test_loss_calculation_prefer_correct_trade_count() -> None:
assert under > correct
def test_loss_calculation_prefer_shorter_trades() -> None:
def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None:
"""
Test Hyperopt.calculate_loss()
"""
@ -99,7 +113,7 @@ def test_loss_calculation_prefer_shorter_trades() -> None:
assert shorter < longer
def test_loss_calculation_has_limited_profit() -> None:
def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
hyperopt = _HYPEROPT
correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20)
@ -109,7 +123,7 @@ def test_loss_calculation_has_limited_profit() -> None:
assert under > correct
def test_log_results_if_loss_improves(capsys) -> None:
def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
@ -124,7 +138,7 @@ def test_log_results_if_loss_improves(capsys) -> None:
assert ' 1/2: foo. Loss 1.00000'in out
def test_no_log_if_loss_does_not_improve(caplog) -> None:
def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2
hyperopt.log_results(
@ -135,7 +149,7 @@ def test_no_log_if_loss_does_not_improve(caplog) -> None:
assert caplog.record_tuples == []
def test_fmin_best_results(mocker, default_conf, caplog) -> None:
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
fmin_result = {
"macd_below_zero": 0,
"adx": 1,
@ -169,6 +183,7 @@ def test_fmin_best_results(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
@ -203,7 +218,7 @@ def test_fmin_best_results(mocker, default_conf, caplog) -> None:
assert line in caplog.text
def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
@ -213,6 +228,8 @@ def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
@ -230,7 +247,7 @@ def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None:
assert line in caplog.text
def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> None:
def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None:
trials = create_trials(mocker)
conf = deepcopy(default_conf)
@ -254,6 +271,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> No
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
@ -272,7 +290,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> No
assert total_tries == (current_tries + len(trials.results))
def test_save_trials_saves_trials(mocker, caplog) -> None:
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None)
@ -281,22 +299,24 @@ def test_save_trials_saves_trials(mocker, caplog) -> None:
hyperopt.save_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has(
'Saving Trials to \'freqtrade/tests/optimize/ut_trials.pickle\'',
'Saving Trials to \'{}\''.format(trials_file),
caplog.record_tuples
)
mock_dump.assert_called_once()
def test_read_trials_returns_trials_file(mocker, caplog) -> None:
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
trials = create_trials(mocker)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials)
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
hyperopt = _HYPEROPT
hyperopt_trial = hyperopt.read_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has(
'Reading Trials from \'freqtrade/tests/optimize/ut_trials.pickle\'',
'Reading Trials from \'{}\''.format(trials_file),
caplog.record_tuples
)
assert hyperopt_trial == trials
@ -304,7 +324,7 @@ def test_read_trials_returns_trials_file(mocker, caplog) -> None:
mock_load.assert_called_once()
def test_roi_table_generation() -> None:
def test_roi_table_generation(init_hyperopt) -> None:
params = {
'roi_t1': 5,
'roi_t2': 10,
@ -318,10 +338,11 @@ def test_roi_table_generation() -> None:
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_start_calls_fmin(mocker, default_conf) -> None:
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
trials = create_trials(mocker)
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
conf = deepcopy(default_conf)
@ -339,7 +360,7 @@ def test_start_calls_fmin(mocker, default_conf) -> None:
mock_fmin.assert_called_once()
def test_start_uses_mongotrials(mocker, default_conf) -> None:
def test_start_uses_mongotrials(mocker, init_hyperopt, default_conf) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
mock_mongotrials = mocker.patch(
@ -354,6 +375,7 @@ def test_start_uses_mongotrials(mocker, default_conf) -> None:
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
hyperopt = Hyperopt(conf)
hyperopt.tickerdata_to_dataframe = MagicMock()
@ -372,9 +394,9 @@ def test_format_results():
Test Hyperopt.format_results()
"""
trades = [
('BTC_ETH', 2, 2, 123),
('BTC_LTC', 1, 1, 123),
('BTC_XRP', -1, -2, -246)
('ETH/BTC', 2, 2, 123),
('LTC/BTC', 1, 1, 123),
('XPR/BTC', -1, -2, -246)
]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
df = pd.DataFrame.from_records(trades, columns=labels)
@ -382,7 +404,7 @@ def test_format_results():
assert x.find(' 66.67%')
def test_signal_handler(mocker):
def test_signal_handler(mocker, init_hyperopt):
"""
Test Hyperopt.signal_handler()
"""
@ -392,11 +414,11 @@ def test_signal_handler(mocker):
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
hyperopt = _HYPEROPT
hyperopt.signal_handler(9, None)
hyperopt.signal_handler(signal.SIGTERM, None)
assert m.call_count == 3
def test_has_space():
def test_has_space(init_hyperopt):
"""
Test Hyperopt.has_space() method
"""
@ -409,14 +431,14 @@ def test_has_space():
assert _HYPEROPT.has_space('buy')
def test_populate_indicators() -> None:
def test_populate_indicators(init_hyperopt) -> None:
"""
Test Hyperopt.populate_indicators()
"""
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
# Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe
@ -424,14 +446,14 @@ def test_populate_indicators() -> None:
assert 'cci' in dataframe
def test_buy_strategy_generator() -> None:
def test_buy_strategy_generator(init_hyperopt) -> None:
"""
Test Hyperopt.buy_strategy_generator()
"""
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist)
dataframe = _HYPEROPT.populate_indicators(dataframes['BTC_UNITEST'])
dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC'])
populate_buy_trend = _HYPEROPT.buy_strategy_generator(
{
@ -481,7 +503,7 @@ def test_buy_strategy_generator() -> None:
assert 1 in result['buy']
def test_generate_optimizer(mocker, default_conf) -> None:
def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
"""
Test Hyperopt.generate_optimizer() function
"""
@ -491,7 +513,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
conf.update({'spaces': 'all'})
trades = [
('BTC_POWR', 0.023117, 0.000233, 100)
('POWR/BTC', 0.023117, 0.000233, 100)
]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
backtest_result = pd.DataFrame.from_records(trades, columns=labels)
@ -500,6 +522,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
'freqtrade.optimize.hyperopt.Hyperopt.backtest',
MagicMock(return_value=backtest_result)
)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock())
optimizer_param = {
'adx': {'enabled': False},

View File

@ -3,15 +3,17 @@
import json
import os
import uuid
import arrow
from shutil import copyfile
from freqtrade import optimize
from freqtrade.misc import file_dump_json
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
load_cached_data_for_updating
from freqtrade.tests.conftest import log_has
# Change this if modifying BTC_UNITEST testdatafile
# Change this if modifying UNITTEST/BTC testdatafile
_BTC_UNITTEST_LENGTH = 13681
@ -52,11 +54,11 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog) -> None:
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
file = 'freqtrade/tests/testdata/BTC_UNITTEST-30.json'
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
_backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_UNITTEST'], ticker_interval=30)
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
assert os.path.isfile(file) is True
assert not log_has('Download the pair: "BTC_ETH", Interval: 30 min', caplog.record_tuples)
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 30m', caplog.record_tuples)
_clean_test_file(file)
@ -66,11 +68,11 @@ def test_load_data_5min_ticker(ticker_history, mocker, caplog) -> None:
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
_backup_file(file, copy_file=True)
optimize.load_data(None, pairs=['BTC_ETH'], ticker_interval=5)
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='5m')
assert os.path.isfile(file) is True
assert not log_has('Download the pair: "BTC_ETH", Interval: 5 min', caplog.record_tuples)
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 5m', caplog.record_tuples)
_clean_test_file(file)
@ -80,11 +82,11 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
_backup_file(file, copy_file=True)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_ETH'])
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
assert os.path.isfile(file) is True
assert not log_has('Download the pair: "BTC_ETH", Interval: 1 min', caplog.record_tuples)
assert not log_has('Download the pair: "UNITTEST/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file)
@ -94,11 +96,25 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
"""
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
_backup_file(file)
optimize.load_data(None, ticker_interval=1, pairs=['BTC_MEME'])
# do not download a new pair if refresh_pairs isn't set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=False,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is False
assert log_has('No data for pair MEME/BTC, use --update-pairs-cached to download the data',
caplog.record_tuples)
# download a new pair if refresh_pairs is set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=True,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is True
assert log_has('Download the pair: "BTC_MEME", Interval: 1 min', caplog.record_tuples)
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file)
@ -109,10 +125,10 @@ def test_testdata_path() -> None:
def test_download_pairs(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
file2_1 = 'freqtrade/tests/testdata/BTC_CFI-1.json'
file2_5 = 'freqtrade/tests/testdata/BTC_CFI-5.json'
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
file2_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-1m.json')
file2_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'CFI_BTC-5m.json')
_backup_file(file1_1)
_backup_file(file1_5)
@ -122,7 +138,7 @@ def test_download_pairs(ticker_history, mocker) -> None:
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 download_pairs(None, pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='1m') is True
assert os.path.isfile(file1_1) is True
assert os.path.isfile(file2_1) is True
@ -134,7 +150,7 @@ def test_download_pairs(ticker_history, mocker) -> None:
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 download_pairs(None, pairs=['MEME/BTC', 'CFI/BTC'], ticker_interval='5m') is True
assert os.path.isfile(file1_5) is True
assert os.path.isfile(file2_5) is True
@ -144,59 +160,166 @@ def test_download_pairs(ticker_history, mocker) -> None:
_clean_test_file(file2_5)
def test_load_cached_data_for_updating(mocker) -> None:
datadir = os.path.join(os.path.dirname(__file__), '..', 'testdata')
test_data = None
test_filename = os.path.join(datadir, 'UNITTEST_BTC-1m.json')
with open(test_filename, "rt") as file:
test_data = json.load(file)
# change now time to test 'line' cases
# now = last cached item + 1 hour
now_ts = test_data[-1][0] / 1000 + 60 * 60
mocker.patch('arrow.utcnow', return_value=arrow.get(now_ts))
# timeframe starts earlier than the cached data
# should fully update data
timerange = (('date', None), test_data[0][0] / 1000 - 1, None)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == []
assert start_ts == test_data[0][0] - 1000
# same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
((None, 'line'), None, -num_lines))
assert data == []
assert start_ts < test_data[0][0] - 1
# timeframe starts in the center of the cached data
# should return the chached data w/o the last item
timerange = (('date', None), test_data[0][0] / 1000 + 1, None)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
timerange = ((None, 'line'), None, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# timeframe starts after the chached data
# should return the chached data w/o the last item
timerange = (('date', None), test_data[-1][0] / 1000 + 1, None)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# same with 'line' timeframe
num_lines = 30
timerange = ((None, 'line'), None, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# no timeframe is set
# should return the chached data w/o the last item
num_lines = 30
timerange = ((None, 'line'), None, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
assert data == test_data[:-1]
assert test_data[-2][0] < start_ts < test_data[-1][0]
# no datafile exist
# should return timestamp start time
timerange = (('date', None), now_ts - 10000, None)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
assert data == []
assert start_ts == (now_ts - 10000) * 1000
# same with 'line' timeframe
num_lines = 30
timerange = ((None, 'line'), None, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
assert data == []
assert start_ts == (now_ts - num_lines * 60) * 1000
# no datafile exist, no timeframe is set
# should return an empty array and None
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
None)
assert data == []
assert start_ts is None
def test_download_pairs_exception(ticker_history, mocker, caplog) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
side_effect=BaseException('File Error'))
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
_backup_file(file1_1)
_backup_file(file1_5)
download_pairs(None, pairs=['BTC-MEME'], ticker_interval=1)
download_pairs(None, pairs=['MEME/BTC'], ticker_interval='1m')
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
assert log_has('Failed to download the pair: "BTC-MEME", Interval: 1 min', caplog.record_tuples)
assert log_has('Failed to download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
def test_download_backtesting_testdata(ticker_history, mocker) -> None:
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
# Download a 1 min ticker file
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
file1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'XEL_BTC-1m.json')
_backup_file(file1)
download_backtesting_testdata(None, pair="BTC-XEL", interval=1)
download_backtesting_testdata(None, pair="XEL/BTC", tick_interval='1m')
assert os.path.isfile(file1) is True
_clean_test_file(file1)
# Download a 5 min ticker file
file2 = 'freqtrade/tests/testdata/BTC_STORJ-5.json'
file2 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'STORJ_BTC-5m.json')
_backup_file(file2)
download_backtesting_testdata(None, pair="BTC-STORJ", interval=5)
download_backtesting_testdata(None, pair="STORJ/BTC", tick_interval='5m')
assert os.path.isfile(file2) is True
_clean_test_file(file2)
def test_download_backtesting_testdata2(mocker) -> None:
tick = [{'T': 'bar'}, {'T': 'foo'}]
tick = [
[1509836520000, 0.00162008, 0.00162008, 0.00162008, 0.00162008, 108.14853839],
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
]
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=tick)
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=1)
download_backtesting_testdata(None, pair="BTC-UNITEST", interval=3)
download_backtesting_testdata(None, pair="UNITTEST/BTC", tick_interval='1m')
download_backtesting_testdata(None, pair="UNITTEST/BTC", tick_interval='3m')
assert json_dump_mock.call_count == 2
def test_load_tickerdata_file() -> None:
# 7 does not exist in either format.
assert not load_tickerdata_file(None, 'BTC_UNITEST', 7)
assert not load_tickerdata_file(None, 'UNITTEST/BTC', '7m')
# 1 exists only as a .json
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
# 8 .json is empty and will fail if it's loaded. .json.gz is a copy of 1.json
tickerdata = load_tickerdata_file(None, 'BTC_UNITEST', 8)
tickerdata = load_tickerdata_file(None, 'UNITTEST/BTC', '8m')
assert _BTC_UNITTEST_LENGTH == len(tickerdata)
@ -207,22 +330,23 @@ def test_init(default_conf, mocker) -> None:
'',
pairs=[],
refresh_pairs=True,
ticker_interval=int(default_conf['ticker_interval'])
ticker_interval=default_conf['ticker_interval']
)
def test_trim_tickerlist() -> None:
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
with open(file) as data_file:
ticker_list = json.load(data_file)
ticker_list_len = len(ticker_list)
# Test the pattern ^(-\d+)$
# This pattern remove X element from the beginning
timerange = ((None, 'line'), None, 5)
# This pattern uses the latest N elements
timerange = ((None, 'line'), None, -5)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_list_len == ticker_len + 5
assert ticker_len == 5
assert ticker_list[0] is not ticker[0] # The first element should be different
assert ticker_list[-1] is ticker[-1] # The last element must be the same
@ -247,6 +371,37 @@ def test_trim_tickerlist() -> None:
assert ticker_list[5] is ticker[0] # The list starts at the index 5
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
# Test the pattern ^(\d{8})-(\d{8})$
# This pattern extract a window between the dates
timerange = (('date', 'date'), ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 5
assert ticker_list[0] is not ticker[0] # The first element should be different
assert ticker_list[5] is ticker[0] # The list starts at the index 5
assert ticker_list[9] is ticker[-1] # The list ends at the index 9 (5 elements)
# Test the pattern ^-(\d{8})$
# This pattern extracts elements from the start to the date
timerange = ((None, 'date'), None, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == 10
assert ticker_list[0] is ticker[0] # The start of the list is included
assert ticker_list[9] is ticker[-1] # The element 10 is not included
# Test the pattern ^(\d{8})-$
# This pattern extracts elements from the date to now
timerange = (('date', None), ticker_list[10][0] / 1000 - 1, None)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
assert ticker_len == ticker_list_len - 10
assert ticker_list[10] is ticker[0] # The first element is element #10
assert ticker_list[-1] is ticker[-1] # The last element is the same
# Test a wrong pattern
# This pattern must return the list unchanged
timerange = ((None, None), None, 5)
@ -261,7 +416,8 @@ def test_file_dump_json() -> None:
Test file_dump_json()
:return: None
"""
file = 'freqtrade/tests/testdata/test_{id}.json'.format(id=str(uuid.uuid4()))
file = os.path.join(os.path.dirname(__file__), '..', 'testdata',
'test_{id}.json'.format(id=str(uuid.uuid4())))
data = {'bar': 'foo'}
# check the file we will create does not exist

View File

@ -25,7 +25,7 @@ def prec_satoshi(a, b) -> float:
# Unit tests
def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
"""
Test rpc_trade_status() method
"""
@ -35,7 +35,8 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -59,7 +60,7 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
result_message = [
'*Trade ID:* `1`\n'
'*Current Pair:* '
'[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
'[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n'
'*Open Since:* `just now`\n'
'*Amount:* `90.99181074`\n'
'*Open Rate:* `0.00001099`\n'
@ -67,13 +68,13 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None:
'*Current Rate:* `0.00001098`\n'
'*Close Profit:* `None`\n'
'*Current Profit:* `-0.59%`\n'
'*Open Order:* `(LIMIT_BUY rem=0.00000000)`'
'*Open Order:* `(limit buy rem=0.00000000)`'
]
assert result == result_message
assert trade.find('[BTC_ETH]') >= 0
assert trade.find('[ETH/BTC]') >= 0
def test_rpc_status_table(default_conf, ticker, mocker) -> None:
def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
"""
Test rpc_status_table() method
"""
@ -83,7 +84,8 @@ def test_rpc_status_table(default_conf, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -102,13 +104,13 @@ def test_rpc_status_table(default_conf, ticker, mocker) -> None:
freqtradebot.create_trade()
(error, result) = rpc.rpc_status_table()
assert 'just now' in result['Since'].all()
assert 'BTC_ETH' in result['Pair'].all()
assert 'ETH/BTC' in result['Pair'].all()
assert '-0.59%' in result['Profit'].all()
assert 'Value' in result
def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker)\
-> None:
def test_rpc_daily_profit(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
"""
Test rpc_daily_profit() method
"""
@ -118,7 +120,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -159,8 +162,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s
assert days.find('must be an integer greater than 0') >= 0
def test_rpc_trade_statistics(
default_conf, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker) -> None:
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
"""
Test rpc_trade_statistics() method
"""
@ -174,7 +177,8 @@ def test_rpc_trade_statistics(
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -215,14 +219,14 @@ def test_rpc_trade_statistics(
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'BTC_ETH'
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
# Test that rpc_trade_statistics can handle trades that lacks
# trade.open_rate (it is set to None)
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_up, limit_buy_order,
limit_sell_order):
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
ticker_sell_up, limit_buy_order, limit_sell_order):
"""
Test rpc_trade_statistics() method
"""
@ -236,7 +240,8 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -254,7 +259,8 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
get_ticker=ticker_sell_up,
get_fee=fee
)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
@ -275,7 +281,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
assert stats['best_pair'] == 'BTC_ETH'
assert stats['best_pair'] == 'ETH/BTC'
assert prec_satoshi(stats['best_rate'], 6.2)
@ -283,22 +289,18 @@ def test_rpc_balance_handle(default_conf, mocker):
"""
Test rpc_balance() method
"""
mock_balance = [
{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
mock_balance = {
'BTC': {
'free': 10.0,
'total': 12.0,
'used': 2.0,
},
{
'Currency': 'ETH',
'Balance': 0.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'ETH': {
'free': 0.0,
'total': 0.0,
'used': 0.0,
}
]
}
patch_get_signal(mocker, (True, False))
mocker.patch.multiple(
@ -319,15 +321,15 @@ def test_rpc_balance_handle(default_conf, mocker):
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
assert not error
(trade, x, y, z) = res
assert prec_satoshi(x, 10)
assert prec_satoshi(z, 150000)
assert prec_satoshi(x, 12)
assert prec_satoshi(z, 180000)
assert 'USD' in y
assert len(trade) == 1
assert 'BTC' in trade[0]['currency']
assert prec_satoshi(trade[0]['available'], 12)
assert prec_satoshi(trade[0]['balance'], 10)
assert prec_satoshi(trade[0]['pending'], 0)
assert prec_satoshi(trade[0]['est_btc'], 10)
assert prec_satoshi(trade[0]['available'], 10)
assert prec_satoshi(trade[0]['balance'], 12)
assert prec_satoshi(trade[0]['pending'], 2)
assert prec_satoshi(trade[0]['est_btc'], 12)
def test_rpc_start(mocker, default_conf) -> None:
@ -386,7 +388,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
assert freqtradebot.state == State.STOPPED
def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
"""
Test rpc_forcesell() method
"""
@ -402,10 +404,12 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
cancel_order=cancel_order_mock,
get_order=MagicMock(
return_value={
'closed': True,
'type': 'LIMIT_BUY',
'status': 'closed',
'type': 'limit',
'side': 'buy'
}
)
),
get_fee=fee,
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -449,8 +453,9 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
return_value={
'closed': None,
'type': 'LIMIT_BUY'
'status': 'open',
'type': 'limit',
'side': 'buy'
}
)
# check that the trade is called, which is done
@ -465,8 +470,9 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
return_value={
'closed': None,
'type': 'LIMIT_SELL'
'status': 'open',
'type': 'limit',
'side': 'sell'
}
)
(error, res) = rpc.rpc_forcesell('2')
@ -476,7 +482,7 @@ def test_rpc_forcesell(default_conf, ticker, mocker) -> None:
assert cancel_order_mock.call_count == 1
def test_performance_handle(default_conf, ticker, limit_buy_order,
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
"""
Test rpc_performance() method
@ -488,7 +494,8 @@ def test_performance_handle(default_conf, ticker, limit_buy_order,
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -510,12 +517,12 @@ def test_performance_handle(default_conf, ticker, limit_buy_order,
(error, res) = rpc.rpc_performance()
assert not error
assert len(res) == 1
assert res[0]['pair'] == 'BTC_ETH'
assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1
assert prec_satoshi(res[0]['profit'], 6.2)
def test_rpc_count(mocker, default_conf, ticker) -> None:
def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
"""
Test rpc_count() method
"""
@ -526,7 +533,8 @@ def test_rpc_count(mocker, default_conf, ticker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_balances=MagicMock(return_value=ticker),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))

View File

@ -234,7 +234,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
)
def test_status(default_conf, update, mocker, ticker) -> None:
def test_status(default_conf, update, mocker, fee, ticker) -> None:
"""
Test _status() method
"""
@ -248,7 +248,9 @@ def test_status(default_conf, update, mocker, ticker) -> None:
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_pair_detail_url=MagicMock(),
get_fee=fee,
)
msg_mock = MagicMock()
status_table = MagicMock()
@ -277,7 +279,7 @@ def test_status(default_conf, update, mocker, ticker) -> None:
assert status_table.call_count == 1
def test_status_handle(default_conf, update, ticker, mocker) -> None:
def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
"""
Test _status() method
"""
@ -286,7 +288,8 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee,
)
msg_mock = MagicMock()
status_table = MagicMock()
@ -319,10 +322,10 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None:
telegram._status(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0]
def test_status_table_handle(default_conf, update, ticker, mocker) -> None:
def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
"""
Test _status_table() method
"""
@ -332,7 +335,8 @@ def test_status_table_handle(default_conf, update, ticker, mocker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_order_id')
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
get_fee=fee,
)
msg_mock = MagicMock()
mocker.patch.multiple(
@ -369,11 +373,11 @@ def test_status_table_handle(default_conf, update, ticker, mocker) -> None:
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
assert int(fields[0]) == 1
assert fields[1] == 'BTC_ETH'
assert fields[1] == 'ETH/BTC'
assert msg_mock.call_count == 1
def test_daily_handle(default_conf, update, ticker, limit_buy_order,
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
limit_sell_order, mocker) -> None:
"""
Test _daily() method
@ -387,7 +391,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order,
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
msg_mock = MagicMock()
mocker.patch.multiple(
@ -484,7 +489,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0]
def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
"""
Test _profit() method
@ -495,7 +500,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
msg_mock = MagicMock()
mocker.patch.multiple(
@ -541,49 +547,42 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up,
assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0]
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
def test_telegram_balance_handle(default_conf, update, mocker) -> None:
"""
Test _balance() method
"""
mock_balance = [
{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
mock_balance = {
'BTC': {
'total': 12.0,
'free': 12.0,
'used': 0.0
},
{
'Currency': 'ETH',
'Balance': 0.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'ETH': {
'total': 0.0,
'free': 0.0,
'used': 0.0
},
{
'Currency': 'USDT',
'Balance': 10000.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'USDT': {
'total': 10000.0,
'free': 10000.0,
'used': 0.0
},
{
'Currency': 'LTC',
'Balance': 10.0,
'Available': 10.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
'LTC': {
'total': 10.0,
'free': 10.0,
'used': 0.0
}
]
}
def mock_ticker(symbol, refresh):
"""
Mock Bittrex.get_ticker() response
"""
if symbol == 'USDT_BTC':
if symbol == 'BTC/USDT':
return {
'bid': 10000.00,
'ask': 10000.00,
@ -615,12 +614,12 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
telegram._balance(bot=MagicMock(), update=update)
result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1
assert '*Currency*: BTC' in result
assert '*Currency*: ETH' not in result
assert '*Currency*: USDT' in result
assert 'Balance' in result
assert 'Est. BTC' in result
assert '*BTC*: 12.00000000' in result
assert '*BTC:*' in result
assert '*ETH:*' not in result
assert '*USDT:*' in result
assert 'Balance:' in result
assert 'Est. BTC:' in result
assert 'BTC: 14.00000000' in result
def test_zero_balance_handle(default_conf, update, mocker) -> None:
@ -630,7 +629,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=[])
mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value={})
msg_mock = MagicMock()
mocker.patch.multiple(
@ -747,7 +746,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker) -> None:
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
"""
Test _forcesell() method
"""
@ -759,7 +758,8 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -779,14 +779,14 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker)
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 '[ETH/BTC]' 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]
def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker) -> None:
def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, mocker) -> None:
"""
Test _forcesell() method
"""
@ -798,7 +798,8 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -822,14 +823,14 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m
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 '[ETH/BTC]' 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_forcesell_all_handle(default_conf, update, ticker, mocker) -> None:
def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None:
"""
Test _forcesell() method
"""
@ -838,10 +839,12 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker) -> None:
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
mocker.patch('freqtrade.exchange.get_pair_detail_url', MagicMock())
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -904,8 +907,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0]
def test_performance_handle(default_conf, update, ticker, limit_buy_order,
limit_sell_order, mocker) -> None:
def test_performance_handle(default_conf, update, ticker, fee,
limit_buy_order, limit_sell_order, mocker) -> None:
"""
Test _performance() method
"""
@ -920,7 +923,8 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order,
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -942,7 +946,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order,
telegram._performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>BTC_ETH\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
assert '<code>ETH/BTC\t6.20% (1)</code>' in msg_mock.call_args_list[0][0][0]
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
@ -968,7 +972,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
assert 'not running' in msg_mock.call_args_list[0][0][0]
def test_count_handle(default_conf, update, ticker, mocker) -> None:
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
"""
Test _count() method
"""
@ -984,8 +988,9 @@ def test_count_handle(default_conf, update, ticker, mocker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_order_id')
buy=MagicMock(return_value={'id': 'mocked_order_id'})
)
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
telegram = Telegram(freqtradebot)

View File

@ -9,7 +9,7 @@ from freqtrade.strategy.default_strategy import DefaultStrategy
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file:
return Analyze.parse_ticker_dataframe(json.load(data_file))
@ -27,7 +27,7 @@ def test_default_strategy(result):
assert type(strategy.minimal_roi) is dict
assert type(strategy.stoploss) is float
assert type(strategy.ticker_interval) is int
assert type(strategy.ticker_interval) is str
indicators = strategy.populate_indicators(result)
assert type(indicators) is DataFrame
assert type(strategy.populate_buy_trend(indicators)) is DataFrame

View File

@ -1,6 +1,7 @@
# pragma pylint: disable=missing-docstring,C0103,protected-access
import freqtrade.tests.conftest as tt # test tools
from unittest.mock import MagicMock
# whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine
@ -12,118 +13,60 @@ def whitelist_conf():
config['stake_currency'] = 'BTC'
config['exchange']['pair_whitelist'] = [
'BTC_ETH',
'BTC_TKN',
'BTC_TRST',
'BTC_SWT',
'BTC_BCC'
'ETH/BTC',
'TKN/BTC',
'TRST/BTC',
'SWT/BTC',
'BCC/BTC'
]
config['exchange']['pair_blacklist'] = [
'BTC_BLK'
'BLK/BTC'
]
return config
def get_market_summaries():
return [{
'MarketName': 'BTC-TKN',
'High': 0.00000919,
'Low': 0.00000820,
'Volume': 74339.61396015,
'Last': 0.00000820,
'BaseVolume': 1664,
'TimeStamp': '2014-07-09T07:19:30.15',
'Bid': 0.00000820,
'Ask': 0.00000831,
'OpenBuyOrders': 15,
'OpenSellOrders': 15,
'PrevDay': 0.00000821,
'Created': '2014-03-20T06:00:00',
'DisplayMarketName': ''
}, {
'MarketName': 'BTC-ETH',
'High': 0.00000072,
'Low': 0.00000001,
'Volume': 166340678.42280999,
'Last': 0.00000005,
'BaseVolume': 42,
'TimeStamp': '2014-07-09T07:21:40.51',
'Bid': 0.00000004,
'Ask': 0.00000005,
'OpenBuyOrders': 18,
'OpenSellOrders': 18,
'PrevDay': 0.00000002,
'Created': '2014-05-30T07:57:49.637',
'DisplayMarketName': ''
}, {
'MarketName': 'BTC-BLK',
'High': 0.00000072,
'Low': 0.00000001,
'Volume': 166340678.42280999,
'Last': 0.00000005,
'BaseVolume': 3,
'TimeStamp': '2014-07-09T07:21:40.51',
'Bid': 0.00000004,
'Ask': 0.00000005,
'OpenBuyOrders': 18,
'OpenSellOrders': 18,
'PrevDay': 0.00000002,
'Created': '2014-05-30T07:57:49.637',
'DisplayMarketName': ''
}]
def get_health():
return [{'Currency': 'ETH', 'IsActive': True},
{'Currency': 'TKN', 'IsActive': True},
{'Currency': 'BLK', 'IsActive': True}]
def get_health_empty():
return []
def test_refresh_market_pair_not_in_whitelist(mocker):
def test_refresh_market_pair_not_in_whitelist(mocker, markets):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
mocker.patch('freqtrade.freqtradebot.exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist(
conf['exchange']['pair_whitelist'] + ['BTC_XXX']
conf['exchange']['pair_whitelist'] + ['XXX/BTC']
)
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist(mocker):
def test_refresh_whitelist(mocker, markets):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health)
mocker.patch('freqtrade.freqtradebot.exchange.get_markets', markets)
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
# List ordered by BaseVolume
whitelist = ['BTC_ETH', 'BTC_TKN']
whitelist = ['ETH/BTC', 'TKN/BTC']
# Ensure all except those in whitelist are removed
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic(mocker):
def test_refresh_whitelist_dynamic(mocker, markets, tickers):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
get_wallet_health=get_health,
get_market_summaries=get_market_summaries
get_markets=markets,
get_tickers=tickers,
exchange_has=MagicMock(return_value=True)
)
# argument: use the whitelist dynamically by exchange-volume
whitelist = ['BTC_TKN', 'BTC_ETH']
whitelist = ['ETH/BTC', 'TKN/BTC']
refreshedwhitelist = freqtradebot._refresh_whitelist(
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
@ -132,10 +75,10 @@ def test_refresh_whitelist_dynamic(mocker):
assert whitelist == refreshedwhitelist
def test_refresh_whitelist_dynamic_empty(mocker):
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty):
conf = whitelist_conf()
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health_empty)
mocker.patch('freqtrade.freqtradebot.exchange.get_markets', markets_empty)
# argument: use the whitelist dynamically by exchange-volume
whitelist = []

View File

@ -50,7 +50,7 @@ def test_dataframe_correct_length(result):
def test_dataframe_correct_columns(result):
assert result.columns.tolist() == \
['date', 'close', 'high', 'low', 'open', 'volume']
['date', 'open', 'high', 'low', 'close', 'volume']
def test_populates_buy_trend(result):
@ -74,7 +74,7 @@ def test_returns_latest_buy_signal(mocker):
return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
assert _ANALYZE.get_signal('ETH/BTC', '5m') == (True, False)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
@ -82,7 +82,7 @@ def test_returns_latest_buy_signal(mocker):
return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
assert _ANALYZE.get_signal('ETH/BTC', '5m') == (False, True)
def test_returns_latest_sell_signal(mocker):
@ -94,7 +94,7 @@ def test_returns_latest_sell_signal(mocker):
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, True)
assert _ANALYZE.get_signal('ETH/BTC', '5m') == (False, True)
mocker.patch.multiple(
'freqtrade.analyze.Analyze',
@ -102,13 +102,13 @@ def test_returns_latest_sell_signal(mocker):
return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}])
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (True, False)
assert _ANALYZE.get_signal('ETH/BTC', '5m') == (True, False)
def test_get_signal_empty(default_conf, mocker, caplog):
caplog.set_level(logging.INFO)
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=None)
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
assert (False, False) == _ANALYZE.get_signal('foo', default_conf['ticker_interval'])
assert log_has('Empty ticker history for pair foo', caplog.record_tuples)
@ -121,7 +121,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog):
side_effect=ValueError('xyz')
)
)
assert (False, False) == _ANALYZE.get_signal('foo', int(default_conf['ticker_interval']))
assert (False, False) == _ANALYZE.get_signal('foo', default_conf['ticker_interval'])
assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples)
@ -134,7 +134,7 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog):
return_value=DataFrame([])
)
)
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
assert (False, False) == _ANALYZE.get_signal('xyz', default_conf['ticker_interval'])
assert log_has('Empty dataframe for pair xyz', caplog.record_tuples)
@ -150,7 +150,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
return_value=DataFrame(ticks)
)
)
assert (False, False) == _ANALYZE.get_signal('xyz', int(default_conf['ticker_interval']))
assert (False, False) == _ANALYZE.get_signal('xyz', default_conf['ticker_interval'])
assert log_has(
'Outdated history for pair xyz. Last tick is 11 minutes old',
caplog.record_tuples
@ -166,20 +166,16 @@ def test_get_signal_handles_exceptions(mocker):
)
)
assert _ANALYZE.get_signal('BTC-ETH', 5) == (False, False)
assert _ANALYZE.get_signal('ETH/BTC', '5m') == (False, False)
def test_parse_ticker_dataframe(ticker_history, ticker_history_without_bv):
columns = ['date', 'close', 'high', 'low', 'open', 'volume']
def test_parse_ticker_dataframe(ticker_history):
columns = ['date', 'open', 'high', 'low', 'close', 'volume']
# Test file with BV data
dataframe = Analyze.parse_ticker_dataframe(ticker_history)
assert dataframe.columns.tolist() == columns
# Test file without BV data
dataframe = Analyze.parse_ticker_dataframe(ticker_history_without_bv)
assert dataframe.columns.tolist() == columns
def test_tickerdata_to_dataframe(default_conf) -> None:
"""
@ -188,7 +184,7 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
analyze = Analyze(default_conf)
timerange = ((None, 'line'), None, -100)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1, timerange=timerange)
tickerlist = {'BTC_UNITEST': tick}
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
data = analyze.tickerdata_to_dataframe(tickerlist)
assert len(data['BTC_UNITEST']) == 100
assert len(data['UNITTEST/BTC']) == 100

View File

@ -55,10 +55,10 @@ def test_parse_args_verbose() -> None:
def test_scripts_options() -> None:
arguments = Arguments(['-p', 'BTC_ETH'], '')
arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.scripts_options()
args = arguments.get_parsed_arg()
assert args.pair == 'BTC_ETH'
assert args.pair == 'ETH/BTC'
def test_parse_args_version() -> None:
@ -109,6 +109,13 @@ def test_parse_args_dynamic_whitelist_invalid_values() -> None:
def test_parse_timerange_incorrect() -> None:
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
assert (('index', 'index'), 200, 500) == Arguments.parse_timerange('200-500')
assert (('date', None), 1274486400, None) == Arguments.parse_timerange('20100522-')
assert ((None, 'date'), None, 1274486400) == Arguments.parse_timerange('-20100522')
timerange = Arguments.parse_timerange('20100522-20150730')
assert timerange == (('date', 'date'), 1274486400, 1438214400)
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
Arguments.parse_timerange('-')
@ -126,7 +133,7 @@ def test_parse_args_backtesting_custom() -> None:
'-c', 'test_conf.json',
'backtesting',
'--live',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--refresh-pairs-cached']
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == 'test_conf.json'
@ -134,7 +141,7 @@ def test_parse_args_backtesting_custom() -> None:
assert call_args.loglevel == logging.INFO
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == 1
assert call_args.ticker_interval == '1m'
assert call_args.refresh_pairs is True

View File

@ -13,6 +13,7 @@ from jsonschema import ValidationError
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.tests.conftest import log_has
from freqtrade import OperationalException
def test_configuration_object() -> None:
@ -28,19 +29,19 @@ def test_configuration_object() -> None:
assert hasattr(Configuration, 'get_config')
def test_load_config_invalid_pair(default_conf, mocker) -> None:
def test_load_config_invalid_pair(default_conf) -> None:
"""
Test the configuration validator with an invalid PAIR format
"""
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'].append('BTC-ETH')
conf['exchange']['pair_whitelist'].append('ETH-BTC')
with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration([])
configuration._validate_config(conf)
def test_load_config_missing_attributes(default_conf, mocker) -> None:
def test_load_config_missing_attributes(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
@ -68,6 +69,21 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
"""
Test Configuration._load_config_file() method
"""
conf = deepcopy(default_conf)
conf['max_open_trades'] = 0
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
read_data=json.dumps(conf)
))
Configuration([])._load_config_file('somefile')
assert file_mock.call_count == 1
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_file_exception(mocker, caplog) -> None:
"""
Test Configuration._load_config_file() method
@ -251,7 +267,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
'backtesting',
'--ticker-interval', '1',
'--ticker-interval', '1m',
'--live',
'--realistic-simulation',
'--refresh-pairs-cached',
@ -276,7 +292,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'ticker_interval' in config
assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples)
assert log_has(
'Using ticker_interval: 1 ...',
'Using ticker_interval: 1m ...',
caplog.record_tuples
)
@ -334,3 +350,29 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
assert 'spaces' in config
assert config['spaces'] == ['all']
assert log_has('Parameter -s/--spaces detected: [\'all\']', caplog.record_tuples)
def test_check_exchange(default_conf) -> None:
"""
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
configuration = Configuration([])
# Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'})
assert configuration.check_exchange(conf)
# Test a valid exchange
conf.get('exchange').update({'name': 'binance'})
assert configuration.check_exchange(conf)
# Test a invalid exchange
conf.get('exchange').update({'name': 'unknown_exchange'})
configuration.config = conf
with pytest.raises(
OperationalException,
match=r'.*Exchange "unknown_exchange" not supported.*'
):
configuration.check_exchange(conf)

View File

@ -6,11 +6,11 @@ from freqtrade.analyze import Analyze
from freqtrade.optimize import load_data
from freqtrade.strategy.resolver import StrategyResolver
_pairs = ['BTC_ETH']
_pairs = ['ETH/BTC']
def load_dataframe_pair(pairs):
ld = load_data(None, ticker_interval=5, pairs=pairs)
ld = load_data(None, ticker_interval='5m', pairs=pairs)
assert isinstance(ld, dict)
assert isinstance(pairs[0], str)
dataframe = ld[pairs[0]]

View File

@ -6,7 +6,10 @@ from unittest.mock import MagicMock
import pytest
from requests.exceptions import RequestException
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
from freqtrade.tests.conftest import patch_coinmarketcap
def test_pair_convertion_object():
@ -77,8 +80,7 @@ def test_fiat_convert_find_price(mocker):
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
with pytest.raises(ValueError, match=r'The crypto symbol XRP is not supported.'):
fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD')
assert fiat_convert.get_price(crypto_symbol='XRP', fiat_symbol='USD') == 0.0
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=12345.0)
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
@ -124,12 +126,38 @@ def test_fiat_convert_get_price(mocker):
assert fiat_convert._pairs[0]._expiration is not expiration
def test_loadcryptomap(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._cryptomap) == 2
assert fiat_convert._cryptomap["BTC"] == "1"
def test_fiat_init_network_exception(mocker):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(side_effect=RequestException)
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
listings=listmock,
)
# with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {}
fiat_convert._load_cryptomap()
assert len(fiat_convert._cryptomap) == 0
def test_fiat_convert_without_network():
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
fiat_convert = CryptoToFiatConverter()
cmc_temp = CryptoToFiatConverter._coinmarketcap
CryptoToFiatConverter._coinmarketcap = None
assert fiat_convert._coinmarketcap is None
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
CryptoToFiatConverter._coinmarketcap = cmc_temp

View File

@ -8,7 +8,6 @@ import logging
import re
import time
from copy import deepcopy
from typing import Dict, Optional
from unittest.mock import MagicMock
import arrow
@ -16,12 +15,11 @@ import pytest
import requests
from sqlalchemy import create_engine
from freqtrade import DependencyException, OperationalException
from freqtrade.exchange import Exchanges
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade
from freqtrade.state import State
from freqtrade.tests.conftest import log_has
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
# Functions for recurrent object patching
@ -65,20 +63,6 @@ def patch_RPCManager(mocker) -> MagicMock:
return rpc_mock
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
"""
Mocker to coinmarketcap to speed up tests
:param mocker: mocker to patch coinmarketcap class
:return: None
"""
mock = MagicMock()
if value:
mock.ticker = {'price_usd': 12345.0}
mocker.patch('freqtrade.fiat_convert.Market', mock)
# Unit tests
def test_freqtradebot_object() -> None:
"""
@ -202,29 +186,28 @@ def test_throttle_with_assets(mocker, default_conf) -> None:
assert result == -1
def test_gen_pair_whitelist(mocker, default_conf, get_market_summaries_data) -> None:
def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None:
"""
Test _gen_pair_whitelist() method
"""
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch(
'freqtrade.freqtradebot.exchange.get_market_summaries',
return_value=get_market_summaries_data
)
mocker.patch('freqtrade.freqtradebot.exchange.get_tickers', tickers)
mocker.patch('freqtrade.freqtradebot.exchange.exchange_has', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
# Test to retrieved BTC sorted on BaseVolume
# Test to retrieved BTC sorted on quoteVolume (default)
whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC')
assert whitelist == ['BTC_ZCL', 'BTC_ZEC', 'BTC_XZC', 'BTC_XWC']
assert whitelist == ['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC']
# Test to retrieved BTC sorted on OpenBuyOrders
whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC', key='OpenBuyOrders')
assert whitelist == ['BTC_XWC', 'BTC_ZCL', 'BTC_ZEC', 'BTC_XZC']
# Test to retrieve BTC sorted on bidVolume
whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC', key='bidVolume')
assert whitelist == ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'BLK/BTC']
# Test with USDT sorted on BaseVolume
# Test with USDT sorted on quoteVolume (default)
whitelist = freqtrade._gen_pair_whitelist(base_currency='USDT')
assert whitelist == ['USDT_XRP', 'USDT_XVG', 'USDT_XMR', 'USDT_ZEC']
assert whitelist == ['TKN/USDT', 'ETH/USDT', 'LTC/USDT', 'BLK/USDT']
# Test with ETH (our fixture does not have ETH, but Bittrex returns them)
# Test with ETH (our fixture does not have ETH, so result should be empty)
whitelist = freqtrade._gen_pair_whitelist(base_currency='ETH')
assert whitelist == []
@ -237,7 +220,7 @@ def test_refresh_whitelist() -> None:
pass
def test_create_trade(default_conf, ticker, limit_buy_order, mocker) -> None:
def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
"""
Test create_trade() method
"""
@ -248,7 +231,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
# Save state of current whitelist
@ -261,7 +245,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker) -> None:
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
assert trade.exchange == 'bittrex'
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
@ -272,19 +256,20 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker) -> None:
assert whitelist == default_conf['exchange']['pair_whitelist']
def test_create_trade_minimal_amount(default_conf, ticker, mocker) -> None:
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
"""
Test create_trade() method
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
buy_mock = MagicMock(return_value='mocked_limit_buy')
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=buy_mock
buy=buy_mock,
get_fee=fee,
)
conf = deepcopy(default_conf)
@ -296,7 +281,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, mocker) -> None:
assert rate * amount >= conf['stake_amount']
def test_create_trade_no_stake_amount(default_conf, ticker, mocker) -> None:
def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
"""
Test create_trade() method
"""
@ -307,8 +292,9 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -316,7 +302,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker) -> None:
freqtrade.create_trade()
def test_create_trade_no_pairs(default_conf, ticker, mocker) -> None:
def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
"""
Test create_trade() method
"""
@ -327,12 +313,13 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade.create_trade()
@ -341,7 +328,8 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker) -> None:
freqtrade.create_trade()
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker) -> None:
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
limit_buy_order, fee, mocker) -> None:
"""
Test create_trade() method
"""
@ -352,12 +340,13 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker) ->
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade.create_trade()
@ -366,7 +355,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker) ->
freqtrade.create_trade()
def test_create_trade_no_signal(default_conf, mocker) -> None:
def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
"""
Test create_trade() method
"""
@ -380,7 +369,8 @@ def test_create_trade_no_signal(default_conf, mocker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker_history=MagicMock(return_value=20),
get_balance=MagicMock(return_value=20)
get_balance=MagicMock(return_value=20),
get_fee=fee,
)
conf = deepcopy(default_conf)
@ -393,7 +383,7 @@ def test_create_trade_no_signal(default_conf, mocker) -> None:
def test_process_trade_creation(default_conf, ticker, limit_buy_order,
health, mocker, caplog) -> None:
markets, fee, mocker, caplog) -> None:
"""
Test the trade creation in _process() method
"""
@ -404,9 +394,10 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order)
get_markets=markets,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_order=MagicMock(return_value=limit_buy_order),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -423,7 +414,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
assert trade.stake_amount == default_conf['stake_amount']
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name
assert trade.exchange == 'bittrex'
assert trade.open_rate == 0.00001099
assert trade.amount == 90.99181073703367
@ -433,7 +424,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
)
def test_process_exchange_failures(default_conf, ticker, health, mocker) -> None:
def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> None:
"""
Test _process() method when a RequestException happens
"""
@ -444,8 +435,8 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker) -> None
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException)
get_markets=markets,
buy=MagicMock(side_effect=TemporaryError)
)
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
@ -455,7 +446,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker) -> None
assert sleep_mock.has_calls()
def test_process_operational_exception(default_conf, ticker, health, mocker) -> None:
def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None:
"""
Test _process() method when an OperationalException happens
"""
@ -466,7 +457,7 @@ def test_process_operational_exception(default_conf, ticker, health, mocker) ->
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
get_markets=markets,
buy=MagicMock(side_effect=OperationalException)
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -478,7 +469,8 @@ def test_process_operational_exception(default_conf, ticker, health, mocker) ->
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker) -> None:
def test_process_trade_handling(
default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None:
"""
Test _process()
"""
@ -489,9 +481,10 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order)
get_markets=markets,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_order=MagicMock(return_value=limit_buy_order),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -560,25 +553,37 @@ def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> No
log_has('Unable to create trade:', caplog.record_tuples)
def test_process_maybe_execute_sell(mocker, default_conf) -> None:
def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplog) -> None:
"""
Test process_maybe_execute_sell() method
"""
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=1)
mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=limit_buy_order)
mocker.patch('freqtrade.freqtradebot.exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_buy_order['amount'])
trade = MagicMock()
trade.open_order_id = '123'
trade.open_fee = 0.001
assert not freqtrade.process_maybe_execute_sell(trade)
# Test amount not modified by fee-logic
assert not log_has('Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(
trade), caplog.record_tuples)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
# test amount modified by fee-logic
assert not freqtrade.process_maybe_execute_sell(trade)
trade.is_open = True
trade.open_order_id = None
# Assert we call handle_trade() if trade is feasible for execution
assert freqtrade.process_maybe_execute_sell(trade)
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker) -> None:
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
"""
Test check_handle() method
"""
@ -592,8 +597,9 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker) -
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee
)
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
@ -604,12 +610,13 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker) -
trade = Trade.query.first()
assert trade
time.sleep(0.01) # Race condition fix
trade.update(limit_buy_order)
assert trade.is_open is True
patch_get_signal(mocker, value=(False, True))
assert freqtrade.handle_trade(trade) is True
assert trade.open_order_id == 'mocked_limit_sell'
assert trade.open_order_id == limit_sell_order['id']
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
@ -620,7 +627,7 @@ 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) -> None:
def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
"""
Test check_handle() method
"""
@ -635,7 +642,8 @@ def test_handle_overlpapping_signals(default_conf, ticker, mocker) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
@ -677,7 +685,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, mocker) -> None:
assert freqtrade.handle_trade(trades[0]) is True
def test_handle_trade_roi(default_conf, ticker, mocker, caplog) -> None:
def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None:
"""
Test check_handle() method
"""
@ -692,7 +700,8 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog) -> None:
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
@ -712,7 +721,8 @@ def test_handle_trade_roi(default_conf, ticker, mocker, caplog) -> None:
assert log_has('Required profit reached. Selling..', caplog.record_tuples)
def test_handle_trade_experimental(default_conf, ticker, mocker, caplog) -> None:
def test_handle_trade_experimental(
default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None:
"""
Test check_handle() method
"""
@ -727,7 +737,8 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog) -> None
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
@ -745,7 +756,7 @@ def test_handle_trade_experimental(default_conf, ticker, mocker, caplog) -> None
assert log_has('Sell signal received. Selling..', caplog.record_tuples)
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker) -> None:
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fee, mocker) -> None:
"""
Test check_handle() method
"""
@ -756,7 +767,8 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -774,7 +786,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
freqtrade.handle_trade(trade)
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mocker) -> None:
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fee, mocker) -> None:
"""
Test check_handle_timedout() method
"""
@ -786,17 +798,19 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mo
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock
cancel_order=cancel_order_mock,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
open_rate=0.00001099,
exchange='BITTREX',
exchange='bittrex',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
fee_open=0.0,
fee_close=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
@ -830,12 +844,13 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
trade_sell = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
open_rate=0.00001099,
exchange='BITTREX',
exchange='bittrex',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
fee_open=0.0,
fee_close=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(hours=-5).datetime,
close_date=arrow.utcnow().shift(minutes=-601).datetime,
@ -869,12 +884,13 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
open_rate=0.00001099,
exchange='BITTREX',
exchange='bittrex',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
fee_open=0.0,
fee_close=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
@ -916,12 +932,13 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
trade_buy = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
open_rate=0.00001099,
exchange='BITTREX',
exchange='bittrex',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
fee_open=0.0,
fee_close=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
@ -929,7 +946,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
Trade.session.add(trade_buy)
regexp = re.compile(
'Cannot query order for Trade(id=1, pair=BTC_ETH, amount=90.99181073, '
'Cannot query order for Trade(id=1, pair=ETH/BTC, amount=90.99181073, '
'open_rate=0.00001099, open_since=10 hours ago) due to Traceback (most '
'recent call last):\n.*'
)
@ -990,7 +1007,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
assert cancel_order_mock.call_count == 1
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker) -> None:
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
"""
Test execute_sell() method with a ticker going UP
"""
@ -1000,7 +1017,8 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker) -> None:
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -1022,7 +1040,7 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker) -> None:
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 '[ETH/BTC]' 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]
@ -1030,7 +1048,7 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker) -> None:
assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0]
def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker) -> None:
def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None:
"""
Test execute_sell() method with a ticker going DOWN
"""
@ -1041,7 +1059,8 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker) -> No
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -1062,14 +1081,15 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker) -> No
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 '[ETH/BTC]' 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_sell_up(default_conf, ticker, ticker_sell_up, mocker) -> None:
def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
ticker_sell_up, mocker) -> None:
"""
Test execute_sell() method with a ticker going DOWN and with a bot config empty
"""
@ -1079,7 +1099,8 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, ticker_sell_up,
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -1101,14 +1122,14 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, ticker_sell_up,
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 '[ETH/BTC]' 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]
def test_execute_sell_without_conf_sell_down(default_conf, ticker,
def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
ticker_sell_down, mocker) -> None:
"""
Test execute_sell() method with a ticker going DOWN and with a bot config empty
@ -1119,7 +1140,8 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker,
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker
get_ticker=ticker,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
@ -1141,12 +1163,12 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker,
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 '[ETH/BTC]' 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_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker) -> None:
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mocker) -> None:
"""
Test sell_profit_only feature when enabled
"""
@ -1162,7 +1184,8 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker) -
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['experimental'] = {
@ -1178,7 +1201,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, mocker) -
assert freqtrade.handle_trade(trade) is True
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker) -> None:
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, mocker) -> None:
"""
Test sell_profit_only feature when disabled
"""
@ -1194,7 +1217,8 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker)
'ask': 0.00002173,
'last': 0.00002172
}),
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['experimental'] = {
@ -1210,7 +1234,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, mocker)
assert freqtrade.handle_trade(trade) is True
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker) -> None:
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
@ -1226,7 +1250,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker) ->
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['experimental'] = {
@ -1242,7 +1267,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, mocker) ->
assert freqtrade.handle_trade(trade) is False
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker) -> None:
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
@ -1258,7 +1283,8 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker) ->
'ask': 0.00000173,
'last': 0.00000172
}),
buy=MagicMock(return_value='mocked_limit_buy')
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
@ -1274,3 +1300,211 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, mocker) ->
trade.update(limit_buy_order)
patch_get_signal(mocker, value=(False, True))
assert freqtrade.handle_trade(trade) is True
def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
"""
Test get_real_amount - fee in quote currency
"""
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order)
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades',
caplog.record_tuples)
def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
"""
Test get_real_amount - fee in quote currency
"""
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[])
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
amount = buy_order_fee['amount']
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) failed: myTrade-Dict empty found',
caplog.record_tuples)
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
"""
Test get_real_amount - fees in Stake currency
"""
trades_for_order[0]['fee']['currency'] = 'ETH'
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker):
"""
Test get_real_amount - Fees in BNB
"""
trades_for_order[0]['fee']['currency'] = 'BNB'
trades_for_order[0]['fee']['cost'] = 0.00094518
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, mocker):
"""
Test get_real_amount with split trades (multiple trades for this order)
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order2)
amount = float(sum(x['amount'] for x in trades_for_order2))
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992) from Trades',
caplog.record_tuples)
def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
"""
Test get_real_amount with split trades (multiple trades for this order)
"""
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'}
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[trades_for_order])
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996) from Order',
caplog.record_tuples)
def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker):
"""
Test get_real_amount with split trades (multiple trades for this order)
"""
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004}
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[])
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker):
"""
Test get_real_amount - fees in Stake currency
"""
# Remove "Currency" from fee dict
trades_for_order[0]['fee'] = {'cost': 0.008}
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount

View File

@ -9,7 +9,7 @@ from unittest.mock import MagicMock
from freqtrade.analyze import Analyze
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray,
common_datearray, file_dump_json)
common_datearray, file_dump_json, format_ms_time)
from freqtrade.optimize.__init__ import load_tickerdata_file
@ -42,21 +42,21 @@ def test_datesarray_to_datetimearray(ticker_history):
assert date_len == 3
def test_common_datearray(default_conf, mocker) -> None:
def test_common_datearray(default_conf) -> None:
"""
Test common_datearray()
:return: None
"""
analyze = Analyze(default_conf)
tick = load_tickerdata_file(None, 'BTC_UNITEST', 1)
tickerlist = {'BTC_UNITEST': tick}
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m')
tickerlist = {'UNITTEST/BTC': tick}
dataframes = analyze.tickerdata_to_dataframe(tickerlist)
dates = common_datearray(dataframes)
assert dates.size == dataframes['BTC_UNITEST']['date'].size
assert dates[0] == dataframes['BTC_UNITEST']['date'][0]
assert dates[-1] == dataframes['BTC_UNITEST']['date'][-1]
assert dates.size == dataframes['UNITTEST/BTC']['date'].size
assert dates[0] == dataframes['UNITTEST/BTC']['date'][0]
assert dates[-1] == dataframes['UNITTEST/BTC']['date'][-1]
def test_file_dump_json(mocker) -> None:
@ -69,3 +69,25 @@ def test_file_dump_json(mocker) -> None:
file_dump_json('somefile', [1, 2, 3])
assert file_open.call_count == 1
assert json_dump.call_count == 1
file_open = mocker.patch('freqtrade.misc.gzip.open', MagicMock())
json_dump = mocker.patch('json.dump', MagicMock())
file_dump_json('somefile', [1, 2, 3], True)
assert file_open.call_count == 1
assert json_dump.call_count == 1
def test_format_ms_time() -> None:
"""
test format_ms_time()
:return: None
"""
# Date 2018-04-10 18:02:01
date_in_epoch_ms = 1523383321000
date = format_ms_time(date_in_epoch_ms)
assert type(date) is str
res = datetime.datetime(2018, 4, 10, 18, 2, 1, tzinfo=datetime.timezone.utc)
assert date == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')
res = datetime.datetime(2017, 12, 13, 8, 2, 1, tzinfo=datetime.timezone.utc)
# Date 2017-12-13 08:02:01
date_in_epoch_ms = 1513152121000
assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S')

View File

@ -4,7 +4,6 @@ import os
import pytest
from sqlalchemy import create_engine
from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade, init, clean_dry_run_db
@ -96,7 +95,7 @@ def test_init_prod_db(default_conf, mocker):
@pytest.mark.usefixtures("init_persistence")
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee):
"""
On this test we will buy and sell a crypto currency.
@ -125,10 +124,11 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
"""
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
assert trade.open_order_id is None
assert trade.open_rate is None
@ -151,12 +151,13 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'something'
@ -174,12 +175,13 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price_exception(limit_buy_order):
def test_calc_close_trade_price_exception(limit_buy_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'something'
@ -190,10 +192,11 @@ def test_calc_close_trade_price_exception(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_update_open_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
)
assert trade.open_order_id is None
@ -201,7 +204,7 @@ def test_update_open_order(limit_buy_order):
assert trade.close_profit is None
assert trade.close_date is None
limit_buy_order['closed'] = False
limit_buy_order['status'] = 'open'
trade.update(limit_buy_order)
assert trade.open_order_id is None
@ -213,10 +216,11 @@ def test_update_open_order(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_update_invalid_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
)
limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
@ -224,12 +228,13 @@ def test_update_invalid_order(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_open_trade_price(limit_buy_order):
def test_calc_open_trade_price(limit_buy_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'open_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -242,12 +247,13 @@ def test_calc_open_trade_price(limit_buy_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'close_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -264,12 +270,13 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit(limit_buy_order, limit_sell_order):
def test_calc_profit(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -295,12 +302,13 @@ def test_calc_profit(limit_buy_order, limit_sell_order):
@pytest.mark.usefixtures("init_persistence")
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -319,40 +327,43 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order):
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
def test_clean_dry_run_db(default_conf):
def test_clean_dry_run_db(default_conf, fee):
init(default_conf, create_engine('sqlite://'))
# Simulate dry_run entries
trade = Trade(
pair='BTC_ETH',
pair='ETH/BTC',
stake_amount=0.001,
amount=123.0,
fee=0.0025,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='BITTREX',
exchange='bittrex',
open_order_id='dry_run_buy_12345'
)
Trade.session.add(trade)
trade = Trade(
pair='BTC_ETC',
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee=0.0025,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='BITTREX',
exchange='bittrex',
open_order_id='dry_run_sell_12345'
)
Trade.session.add(trade)
# Simulate prod entry
trade = Trade(
pair='BTC_ETC',
pair='ETC/BTC',
stake_amount=0.001,
amount=123.0,
fee=0.0025,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='BITTREX',
exchange='bittrex',
open_order_id='prod_buy_12345'
)
Trade.session.add(trade)
@ -364,3 +375,105 @@ def test_clean_dry_run_db(default_conf):
# We have now only the prod
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
def test_migrate_old(default_conf, fee):
"""
Test Database migration(starting with old pairformat)
"""
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 1, {fee},
0.00258580, {stake}, {amount},
'2017-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# Run init to test migration
init(default_conf, engine)
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "bittrex"
def test_migrate_new(default_conf, fee):
"""
Test Database migration (starting with new pairformat)
"""
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# Run init to test migration
init(default_conf, engine)
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""This script generate json data from bittrex"""
import json
import sys
from freqtrade import exchange
from freqtrade import misc
from freqtrade.exchange import Bittrex
parser = misc.common_args_parser('download utility')
parser.add_argument(
'-p', '--pair',
help='JSON file containing pairs to download',
dest='pair',
default=None
)
args = parser.parse_args(sys.argv[1:])
TICKER_INTERVALS = [1, 5] # ticker interval in minutes (currently implemented: 1 and 5)
PAIRS = []
if args.pair:
with open(args.pair) as file:
PAIRS = json.load(file)
PAIRS = list(set(PAIRS))
print('About to download pairs:', PAIRS)
# Init Bittrex exchange
exchange._API = Bittrex({'key': '', 'secret': ''})
for pair in PAIRS:
for tick_interval in TICKER_INTERVALS:
print('downloading pair %s, interval %s' % (pair, tick_interval))
data = exchange.get_ticker_history(pair, tick_interval)
filename = '{}-{}.json'.format(pair, tick_interval)
misc.file_dump_json(filename, data)

View File

@ -1,26 +1,26 @@
[
"BTC_ADA",
"BTC_BAT",
"BTC_DASH",
"BTC_ETC",
"BTC_ETH",
"BTC_GBYTE",
"BTC_LSK",
"BTC_LTC",
"BTC_NEO",
"BTC_NXT",
"BTC_POWR",
"BTC_STORJ",
"BTC_QTUM",
"BTC_WAVES",
"BTC_VTC",
"BTC_XLM",
"BTC_XMR",
"BTC_XVG",
"BTC_XRP",
"BTC_ZEC",
"USDT_BTC",
"USDT_LTC",
"USDT_ETH"
"ADA/BTC",
"BAT/BTC",
"DASH/BTC",
"ETC/BTC",
"ETH/BTC",
"GBYTE/BTC",
"LSK/BTC",
"LTC/BTC",
"NEO/BTC",
"NXT/BTC",
"POWR/BTC",
"STORJ/BTC",
"QTUM/BTC",
"WAVES/BTC",
"VTC/BTC",
"XLM/BTC",
"XMR/BTC",
"XVG/BTC",
"XRP/BTC",
"ZEC/BTC",
"BTC/USDT",
"LTC/USDT",
"ETH/USDT"
]

View File

@ -1,25 +1,25 @@
python-bittrex==0.3.0
SQLAlchemy==1.2.7
python-telegram-bot==10.0.2
ccxt==1.14.96
SQLAlchemy==1.2.8
python-telegram-bot==10.1.0
arrow==0.12.1
cachetools==2.0.1
cachetools==2.1.0
requests==2.18.4
urllib3==1.22
wrapt==1.10.11
pandas==0.22.0
pandas==0.23.0
scikit-learn==0.19.1
scipy==1.0.1
scipy==1.1.0
jsonschema==2.6.0
numpy==1.14.3
TA-Lib==0.4.17
pytest==3.5.1
pytest==3.6.0
pytest-mock==1.10.0
pytest-cov==2.5.1
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
coinmarketcap==4.2.1
coinmarketcap==5.0.3
# Required for plotting data
#plotly==2.3.0

201
scripts/convert_backtestdata.py Executable file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
Script to display when the bot will buy a specific pair
Mandatory Cli parameters:
-p / --pair: pair to examine
Optional Cli parameters
-d / --datadir: path to pair backtest data
--timerange: specify what timerange of data to use.
-l / --live: Live, to download the latest ticker for the pair
"""
import logging
import sys
from argparse import Namespace
from os import path
import glob
import json
import re
from typing import List, Dict
import gzip
from freqtrade.arguments import Arguments
from freqtrade import misc, constants
from pandas import DataFrame
import dateutil.parser
logger = logging.getLogger('freqtrade')
def load_old_file(filename) -> (List[Dict], bool):
if not path.isfile(filename):
logger.warning("filename %s does not exist", filename)
return (None, False)
logger.debug('Loading ticker data from file %s', filename)
pairdata = None
if filename.endswith('.gz'):
logger.debug('Loading ticker data from file %s', filename)
is_zip = True
with gzip.open(filename) as tickerdata:
pairdata = json.load(tickerdata)
else:
is_zip = False
with open(filename) as tickerdata:
pairdata = json.load(tickerdata)
return (pairdata, is_zip)
def parse_old_backtest_data(ticker) -> DataFrame:
"""
Reads old backtest data
Format: "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",
"BV": 0.0877869
"""
columns = {'C': 'close', 'V': 'volume', 'O': 'open',
'H': 'high', 'L': 'low', 'T': 'date'}
frame = DataFrame(ticker) \
.rename(columns=columns)
if 'BV' in frame:
frame.drop('BV', 1, inplace=True)
if 'date' not in frame:
logger.warning("Date not in frame - probably not a Ticker file")
return None
frame.sort_values('date', inplace=True)
return frame
def convert_dataframe(frame: DataFrame):
"""Convert dataframe to new format"""
# reorder columns:
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
frame = frame[cols]
# Make sure parsing/printing data is assumed to be UTC
frame['date'] = frame['date'].apply(
lambda d: int(dateutil.parser.parse(d+'+00:00').timestamp()) * 1000)
frame['date'] = frame['date'].astype('int64')
# Convert columns one by one to preserve type.
by_column = [frame[x].values.tolist() for x in frame.columns]
return list(list(x) for x in zip(*by_column))
def convert_file(filename: str, filename_new: str) -> None:
"""Converts a file from old format to ccxt format"""
(pairdata, is_zip) = load_old_file(filename)
if pairdata and type(pairdata) is list:
if type(pairdata[0]) is list:
logger.error("pairdata for %s already in new format", filename)
return
frame = parse_old_backtest_data(pairdata)
# Convert frame to new format
if frame is not None:
frame1 = convert_dataframe(frame)
misc.file_dump_json(filename_new, frame1, is_zip)
def convert_main(args: Namespace) -> None:
"""
converts a folder given in --datadir from old to new format to support ccxt
"""
workdir = path.join(args.datadir, "")
logger.info("Workdir: %s", workdir)
for filename in glob.glob(workdir + "*.json"):
# swap currency names
ret = re.search(r'[A-Z_]{7,}', path.basename(filename))
if args.norename:
filename_new = filename
else:
if not ret:
logger.warning("file %s could not be converted, could not extract currencies",
filename)
continue
pair = ret.group(0)
currencies = pair.split("_")
if len(currencies) != 2:
logger.warning("file %s could not be converted, could not extract currencies",
filename)
continue
ret_integer = re.search(r'\d+(?=\.json)', path.basename(filename))
ret_string = re.search(r'(\d+[mhdw])(?=\.json)', path.basename(filename))
if ret_integer:
minutes = int(ret_integer.group(0))
# default to adding 'm' to end of minutes for new interval name
interval = str(minutes) + 'm'
# but check if there is a mapping between int and string also
for str_interval, minutes_interval in constants.TICKER_INTERVAL_MINUTES.items():
if minutes_interval == minutes:
interval = str_interval
break
# change order on pairs if old ticker interval found
filename_new = path.join(path.dirname(filename),
"{}_{}-{}.json".format(currencies[1],
currencies[0], interval))
elif ret_string:
interval = ret_string.group(0)
filename_new = path.join(path.dirname(filename),
"{}_{}-{}.json".format(currencies[0],
currencies[1], interval))
else:
logger.warning("file %s could not be converted, interval not found", filename)
continue
logger.debug("Converting and renaming %s to %s", filename, filename_new)
convert_file(filename, filename_new)
def convert_parse_args(args: List[str]) -> Namespace:
"""
Parse args passed to the script
:param args: Cli arguments
:return: args: Array with all arguments
"""
arguments = Arguments(args, 'Convert datafiles')
arguments.parser.add_argument(
'-d', '--datadir',
help='path to backtest data (default: %(default)s',
dest='datadir',
default=path.join('freqtrade', 'tests', 'testdata'),
type=str,
metavar='PATH',
)
arguments.parser.add_argument(
'-n', '--norename',
help='don''t rename files from BTC_<PAIR> to <PAIR>_BTC - '
'Note that not renaming will overwrite source files',
dest='norename',
default=False,
action='store_true'
)
return arguments.parse_args()
def main(sysargv: List[str]) -> None:
"""
This function will initiate the bot and start the trading loop.
:return: None
"""
logger.info('Starting Dataframe conversation')
convert_main(convert_parse_args(sysargv))
if __name__ == '__main__':
main(sys.argv[1:])

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""This script generate json data from bittrex"""
import json
import sys
import os
import arrow
from freqtrade import (exchange, arguments, misc)
DEFAULT_DL_PATH = 'freqtrade/tests/testdata'
arguments = arguments.Arguments(sys.argv[1:], 'download utility')
arguments.testdata_dl_options()
args = arguments.parse_args()
TICKER_INTERVALS = ['1m', '5m']
PAIRS = []
if args.pairs_file:
with open(args.pairs_file) as file:
PAIRS = json.load(file)
PAIRS = list(set(PAIRS))
dl_path = DEFAULT_DL_PATH
if args.export and os.path.exists(args.export):
dl_path = args.export
since_time = None
if args.days:
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
print(f'About to download pairs: {PAIRS} to {dl_path}')
# Init exchange
exchange._API = exchange.init_ccxt({'key': '',
'secret': '',
'name': args.exchange})
for pair in PAIRS:
for tick_interval in TICKER_INTERVALS:
print(f'downloading pair {pair}, interval {tick_interval}')
data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time)
if not data:
print('\tNo data was downloaded')
break
print('\tData was downloaded for period %s - %s' % (
arrow.get(data[0][0] / 1000).format(),
arrow.get(data[-1][0] / 1000).format()))
# save data
pair_print = pair.replace('/', '_')
filename = f'{pair_print}-{tick_interval}.json'
misc.file_dump_json(os.path.join(dl_path, filename), data)

View File

@ -10,6 +10,7 @@ Optional Cli parameters
-d / --datadir: path to pair backtest data
--timerange: specify what timerange of data to use.
-l / --live: Live, to download the latest ticker for the pair
-db / --db-url: Show trades stored in database
"""
import logging
import sys
@ -21,14 +22,18 @@ from plotly import tools
from plotly.offline import plot
import plotly.graph_objs as go
from typing import Dict, List, Any
from sqlalchemy import create_engine
from freqtrade.arguments import Arguments
from freqtrade.analyze import Analyze
from freqtrade import exchange
import freqtrade.optimize as optimize
from freqtrade import persistence
from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
_CONF: Dict[str, Any] = {}
def plot_analyzed_dataframe(args: Namespace) -> None:
"""
@ -54,7 +59,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
if args.live:
logger.info('Downloading pair.')
# Init Bittrex to use public API
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
exchange.init({'key': '', 'secret': ''})
tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
else:
tickers = optimize.load_data(
@ -69,6 +74,12 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
dataframe = analyze.populate_buy_trend(dataframe)
dataframe = analyze.populate_sell_trend(dataframe)
trades = []
if args.db_url:
engine = create_engine('sqlite:///' + args.db_url)
persistence.init(_CONF, engine)
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
if len(dataframe.index) > 750:
logger.warning('Ticker contained more than 750 candles, clipping.')
data = dataframe.tail(750)
@ -109,6 +120,31 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
)
)
trade_buys = go.Scattergl(
x=[t.open_date.isoformat() for t in trades],
y=[t.open_rate for t in trades],
mode='markers',
name='trade_buy',
marker=dict(
symbol='square-open',
size=11,
line=dict(width=2),
color='green'
)
)
trade_sells = go.Scattergl(
x=[t.close_date.isoformat() for t in trades],
y=[t.close_rate for t in trades],
mode='markers',
name='trade_sell',
marker=dict(
symbol='square-open',
size=11,
line=dict(width=2),
color='red'
)
)
bb_lower = go.Scatter(
x=data.date,
y=data.bb_lowerband,
@ -154,6 +190,8 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
fig.append_trace(volume, 2, 1)
fig.append_trace(macd, 3, 1)
fig.append_trace(macdsignal, 3, 1)
fig.append_trace(trade_buys, 1, 1)
fig.append_trace(trade_sells, 1, 1)
fig['layout'].update(title=args.pair)
fig['layout']['yaxis1'].update(title='Price')

View File

@ -24,6 +24,7 @@ import plotly.graph_objs as go
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.analyze import Analyze
from freqtrade import constants
import freqtrade.optimize as optimize
import freqtrade.misc as misc
@ -33,10 +34,10 @@ logger = logging.getLogger(__name__)
# data:: [ pair, profit-%, enter, exit, time, duration]
# data:: ["BTC_ETH", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65]
def make_profit_array(
data: List, px: int, min_date: int,
interval: int, filter_pairs: Optional[List] = None) -> np.ndarray:
# data:: ["ETH/BTC", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65]
def make_profit_array(data: List, px: int, min_date: int,
interval: int,
filter_pairs: Optional[List] = None) -> np.ndarray:
pg = np.zeros(px)
filter_pairs = filter_pairs or []
# Go through the trades
@ -186,11 +187,12 @@ def plot_profit(args: Namespace) -> None:
plot(fig, filename='freqtrade-profit-plot.html')
def define_index(min_date: int, max_date: int, interval: int) -> int:
def define_index(min_date: int, max_date: int, interval: str) -> int:
"""
Return the index of a specific date
"""
return int((max_date - min_date) / (interval * 60))
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
return int((max_date - min_date) / (interval_minutes * 60))
def plot_parse_args(args: List[str]) -> Namespace:

View File

@ -21,7 +21,7 @@ setup(name='freqtrade',
setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[
'python-bittrex',
'ccxt',
'SQLAlchemy',
'python-telegram-bot',
'arrow',

View File

@ -89,13 +89,21 @@ function config_generator () {
echo "General configuration"
echo "-------------------------"
echo
read -p "Max open trades: (Default: 3) " max_trades
default_max_trades=3
read -p "Max open trades: (Default: $default_max_trades) " max_trades
max_trades=${max_trades:-$default_max_trades}
read -p "Stake amount: (Default: 0.05) " stake_amount
default_stake_amount=0.05
read -p "Stake amount: (Default: $default_stake_amount) " stake_amount
stake_amount=${stake_amount:-$default_stake_amount}
read -p "Stake currency: (Default: BTC) " stake_currency
default_stake_currency="BTC"
read -p "Stake currency: (Default: $default_stake_currency) " stake_currency
stake_currency=${stake_currency:-$default_stake_currency}
read -p "Fiat currency: (Default: USD) " fiat_currency
default_fiat_currency="USD"
read -p "Fiat currency: (Default: $default_fiat_currency) " fiat_currency
fiat_currency=${fiat_currency:-$default_fiat_currency}
echo "------------------------"
echo "Bittrex config generator"

View File

@ -25,17 +25,18 @@ def hyperopt_optimize_conf() -> dict:
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"pair_whitelist": [
"BTC_ETH",
"BTC_LTC",
"BTC_ETC",
"BTC_DASH",
"BTC_ZEC",
"BTC_XLM",
"BTC_NXT",
"BTC_POWR",
"BTC_ADA",
"BTC_XMR"
"ETH/BTC",
"LTC/BTC",
"ETC/BTC",
"DASH/BTC",
"ZEC/BTC",
"XLM/BTC",
"NXT/BTC",
"POWR/BTC",
"ADA/BTC",
"XMR/BTC"
]
}
}

View File

@ -41,7 +41,7 @@ class TestStrategy(IStrategy):
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = 5
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""