Merge with develop

This commit is contained in:
Anton 2018-06-04 01:50:10 +03:00
commit 87f750da35
39 changed files with 506 additions and 250 deletions

1
.gitignore vendored
View File

@ -90,3 +90,4 @@ target/
.vscode .vscode
.pytest_cache/ .pytest_cache/
.mypy_cache/

View File

@ -13,7 +13,7 @@ addons:
install: install:
- ./install_ta-lib.sh - ./install_ta-lib.sh
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install --upgrade flake8 coveralls pytest-random-order - pip install --upgrade flake8 coveralls pytest-random-order mypy
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install -e . - pip install -e .
jobs: jobs:
@ -26,6 +26,7 @@ jobs:
- cp config.json.example config.json - cp config.json.example config.json
- python freqtrade/main.py hyperopt -e 5 - python freqtrade/main.py hyperopt -e 5
- script: flake8 freqtrade - script: flake8 freqtrade
- script: mypy freqtrade
after_success: after_success:
- coveralls - coveralls
notifications: notifications:

View File

@ -42,4 +42,16 @@ pip3.6 install flake8 coveralls
flake8 freqtrade flake8 freqtrade
``` ```
## 3. Test if all type-hints are correct
**Install packages** (If not already installed)
``` bash
pip3.6 install mypy
```
**Run mypy**
``` bash
mypy freqtrade
```

View File

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

View File

@ -53,9 +53,9 @@ python3 ./freqtrade/main.py backtesting --datadir freqtrade/tests/testdata-20180
**With a (custom) strategy file** **With a (custom) strategy file**
```bash ```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** **Exporting trades to file**
```bash ```bash
@ -83,6 +83,8 @@ The full timerange specification:
- Use tickframes till 2018/01/31: `--timerange=-20180131` - Use tickframes till 2018/01/31: `--timerange=-20180131`
- Use tickframes since 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` - Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
- Use tickframes between POSIX timestamps 1527595200 1527618600:
`--timerange=1527595200-1527618600`
**Update testdata directory** **Update testdata directory**

View File

@ -9,7 +9,8 @@ it.
## Bot commands ## 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] [--dry-run-db]
{backtesting,hyperopt} ... {backtesting,hyperopt} ...
@ -26,17 +27,18 @@ optional arguments:
--version show program's version number and exit --version show program's version number and exit
-c PATH, --config PATH -c PATH, --config PATH
specify configuration file (default: config.json) specify configuration file (default: config.json)
-d PATH, --datadir PATH
path to backtest data (default:
freqtrade/tests/testdata
-s NAME, --strategy NAME -s NAME, --strategy NAME
specify strategy class name (default: DefaultStrategy) specify strategy class name (default: DefaultStrategy)
--strategy-path PATH specify additional strategy lookup path --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] --dynamic-whitelist [INT]
dynamically generate and update whitelist based on 24h dynamically generate and update whitelist based on 24h
BaseVolume (Default 20 currencies) 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? ### How to use a different config file?
@ -116,21 +118,25 @@ python3 ./freqtrade/main.py -c config.json --dry-run-db
Backtesting also uses the config specified via `-c/--config`. Backtesting also uses the config specified via `-c/--config`.
``` ```
usage: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation] usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
[-r] [--timerange TIMERANGE] [-l] [-r] [--export EXPORT]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-l, --live using live data -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
-i INT, --ticker-interval INT specify ticker interval (1m, 5m, 30m, 1h, 1d)
specify ticker interval (default: '5m')
--realistic-simulation --realistic-simulation
uses max_open_trades from config to simulate real uses max_open_trades from config to simulate real
world limitations world limitations
--timerange TIMERANGE
specify what timerange of data to use.
-l, --live using live data
-r, --refresh-pairs-cached -r, --refresh-pairs-cached
refresh the pairs files in tests/testdata with refresh the pairs files in tests/testdata with the
the latest data from the exchange. Use it if you want latest data from the exchange. Use it if you want to
to run your backtesting with up-to-date data. run your backtesting with up-to-date data.
--export EXPORT export backtest results, argument are: trades Example
--export=trades
``` ```
### How to use --refresh-pairs-cached parameter? ### How to use --refresh-pairs-cached parameter?
@ -153,14 +159,25 @@ Hyperopt uses an internal json config return by `hyperopt_optimize_conf()`
located in `freqtrade/optimize/hyperopt_conf.py`. located in `freqtrade/optimize/hyperopt_conf.py`.
``` ```
usage: freqtrade hyperopt [-h] [-e INT] [--use-mongodb] usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation]
[--timerange TIMERANGE] [-e INT] [--use-mongodb]
[-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
specify ticker interval (1m, 5m, 30m, 1h, 1d)
--realistic-simulation
uses max_open_trades from config to simulate real
world limitations
--timerange TIMERANGE
specify what timerange of data to use.
-e INT, --epochs INT specify number of epochs (default: 100) -e INT, --epochs INT specify number of epochs (default: 100)
--use-mongodb parallelize evaluations with mongodb (requires mongod --use-mongodb parallelize evaluations with mongodb (requires mongod
in PATH) in PATH)
-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]
Specify which parameters to hyperopt. Space separate
list. Default: all
``` ```
## A parameter missing in the configuration? ## A parameter missing in the configuration?

View File

@ -24,7 +24,7 @@ The table below will list all configuration parameters.
| `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. | `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. | `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. | `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.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.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. | `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
@ -86,6 +86,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 price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary. 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? ### What values for fiat_display_currency?
`fiat_display_currency` set the fiat to use for the conversion form coin to fiat in Telegram. `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". 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".
@ -103,7 +115,7 @@ creating trades.
"dry_run": true, "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 ```json
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@ -129,7 +141,7 @@ you run it in production mode.
"dry_run": false, "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 ```json
"exchange": { "exchange": {
"name": "bittrex", "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 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. In this example, the database will be created inside the docker instance and will be lost when you will refresh your image.

15
freqtrade/__main__.py Normal file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.6)
"""
import sys
from freqtrade import main
if __name__ == '__main__':
main.set_loggers()
main.main(sys.argv[1:])

View File

@ -12,7 +12,7 @@ from pandas import DataFrame, to_datetime
from freqtrade import constants from freqtrade import constants
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver from freqtrade.strategy.resolver import StrategyResolver, IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -37,7 +37,7 @@ class Analyze(object):
:param config: Bot configuration (use the one from Configuration()) :param config: Bot configuration (use the one from Configuration())
""" """
self.config = config self.config = config
self.strategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver(self.config).strategy
@staticmethod @staticmethod
def parse_ticker_dataframe(ticker: list) -> DataFrame: def parse_ticker_dataframe(ticker: list) -> DataFrame:

View File

@ -17,9 +17,9 @@ class Arguments(object):
Arguments Class. Manage the arguments received by the cli Arguments Class. Manage the arguments received by the cli
""" """
def __init__(self, args: List[str], description: str): def __init__(self, args: List[str], description: str) -> None:
self.args = args self.args = args
self.parsed_arg = None self.parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description) self.parser = argparse.ArgumentParser(description=description)
def _load_args(self) -> None: def _load_args(self) -> None:
@ -211,7 +211,8 @@ class Arguments(object):
self.hyperopt_options(hyperopt_cmd) self.hyperopt_options(hyperopt_cmd)
@staticmethod @staticmethod
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]: def parse_timerange(text: Optional[str]) -> Optional[Tuple[Tuple,
Optional[int], Optional[int]]]:
""" """
Parse the value of the argument --timerange to determine what is the range desired Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange :param text: value from --timerange
@ -222,6 +223,9 @@ class Arguments(object):
syntax = [(r'^-(\d{8})$', (None, 'date')), syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)), (r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')), (r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')), (r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)), (r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))] (r'^(\d+)-(\d+)$', ('index', 'index'))]
@ -231,21 +235,23 @@ class Arguments(object):
if match: # Regex has matched if match: # Regex has matched
rvals = match.groups() rvals = match.groups()
index = 0 index = 0
start = None start: Optional[int] = None
stop = None stop: Optional[int] = None
if stype[0]: if stype[0]:
start = rvals[index] starts = rvals[index]
if stype[0] == 'date': if stype[0] == 'date':
start = arrow.get(start, 'YYYYMMDD').timestamp start = int(starts) if len(starts) == 10 \
else arrow.get(starts, 'YYYYMMDD').timestamp
else: else:
start = int(start) start = int(starts)
index += 1 index += 1
if stype[1]: if stype[1]:
stop = rvals[index] stops = rvals[index]
if stype[1] == 'date': if stype[1] == 'date':
stop = arrow.get(stop, 'YYYYMMDD').timestamp stop = int(stops) if len(stops) == 10 \
else arrow.get(stops, 'YYYYMMDD').timestamp
else: else:
stop = int(stop) stop = int(stops)
return stype, start, stop return stype, start, stop
raise Exception('Incorrect syntax for timerange "%s"' % text) raise Exception('Incorrect syntax for timerange "%s"' % text)

View File

@ -5,7 +5,7 @@ This module contains the configuration class
import json import json
import logging import logging
from argparse import Namespace from argparse import Namespace
from typing import Dict, Any from typing import Optional, Dict, Any
from jsonschema import Draft4Validator, validate from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match from jsonschema.exceptions import ValidationError, best_match
import ccxt import ccxt
@ -23,7 +23,7 @@ class Configuration(object):
""" """
def __init__(self, args: Namespace) -> None: def __init__(self, args: Namespace) -> None:
self.args = args self.args = args
self.config = None self.config: Optional[Dict[str, Any]] = None
def load_config(self) -> Dict[str, Any]: def load_config(self) -> Dict[str, Any]:
""" """
@ -145,7 +145,7 @@ class Configuration(object):
# If --datadir is used we add it to the configuration # If --datadir is used we add it to the configuration
if 'datadir' in self.args and self.args.datadir: if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self.args.datadir}) config.update({'datadir': self.args.datadir})
logger.info('Parameter --datadir detected: %s ...', self.args.datadir) logger.info('Using data folder: %s ...', self.args.datadir)
# If -r/--refresh-pairs-cached is used we add it to the configuration # If -r/--refresh-pairs-cached is used we add it to the configuration
if 'refresh_pairs' in self.args and self.args.refresh_pairs: if 'refresh_pairs' in self.args and self.args.refresh_pairs:
@ -192,7 +192,7 @@ class Configuration(object):
validate(conf, constants.CONF_SCHEMA) validate(conf, constants.CONF_SCHEMA)
return conf return conf
except ValidationError as exception: except ValidationError as exception:
logger.fatal( logger.critical(
'Invalid configuration. See config.json.example. Reason: %s', 'Invalid configuration. See config.json.example. Reason: %s',
exception exception
) )

View File

@ -25,6 +25,12 @@ TICKER_INTERVAL_MINUTES = {
'1w': 10080, '1w': 10080,
} }
SUPPORTED_FIAT = [
"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"
]
# Required json-schema for user specified config # Required json-schema for user specified config
CONF_SCHEMA = { CONF_SCHEMA = {
@ -32,20 +38,13 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 0}, 'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, 'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': { 'stake_amount': {
"type": ["number", "string"], "type": ["number", "string"],
"minimum": 0.0005, "minimum": 0.0005,
"pattern": UNLIMITED_STAKE_AMOUNT "pattern": UNLIMITED_STAKE_AMOUNT
}, },
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF', 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'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']},
'dry_run': {'type': 'boolean'}, 'dry_run': {'type': 'boolean'},
'minimal_roi': { 'minimal_roi': {
'type': 'object', 'type': 'object',

View File

@ -290,10 +290,15 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
# chached data was already downloaded # chached data was already downloaded
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000) till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
data = [] data: List[Dict[Any, Any]] = []
while not since_ms or since_ms < till_time_ms: while not since_ms or since_ms < till_time_ms:
data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms) data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
# Because some exchange sort Tickers ASC and other DESC.
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
# when GDAX returns a list of tickers DESC (newest first, oldest last)
data_part = sorted(data_part, key=lambda x: x[0])
if not data_part: if not data_part:
break break

View File

@ -5,9 +5,11 @@ e.g BTC to USD
import logging import logging
import time import time
from typing import Dict from typing import Dict, List
from coinmarketcap import Market from coinmarketcap import Market
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,7 +35,7 @@ class CryptoFiat(object):
self.price = 0.0 self.price = 0.0
# Private attributes # Private attributes
self._expiration = 0 self._expiration = 0.0
self.crypto_symbol = crypto_symbol.upper() self.crypto_symbol = crypto_symbol.upper()
self.fiat_symbol = fiat_symbol.upper() self.fiat_symbol = fiat_symbol.upper()
@ -64,15 +66,7 @@ class CryptoToFiatConverter(object):
This object is also a Singleton This object is also a Singleton
""" """
__instance = None __instance = None
_coinmarketcap = None _coinmarketcap: Market = None
# Constants
SUPPORTED_FIAT = [
"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"
]
_cryptomap: Dict = {} _cryptomap: Dict = {}
@ -86,7 +80,7 @@ class CryptoToFiatConverter(object):
return CryptoToFiatConverter.__instance return CryptoToFiatConverter.__instance
def __init__(self) -> None: def __init__(self) -> None:
self._pairs = [] self._pairs: List[CryptoFiat] = []
self._load_cryptomap() self._load_cryptomap()
def _load_cryptomap(self) -> None: def _load_cryptomap(self) -> None:
@ -94,8 +88,11 @@ class CryptoToFiatConverter(object):
coinlistings = self._coinmarketcap.listings() coinlistings = self._coinmarketcap.listings()
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])), self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
coinlistings["data"])) coinlistings["data"]))
except ValueError: except (ValueError, RequestException) as exception:
logger.error("Could not load FIAT Cryptocurrency map") logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: %s",
exception
)
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
""" """
@ -174,7 +171,7 @@ class CryptoToFiatConverter(object):
fiat = fiat.upper() fiat = fiat.upper()
return fiat in self.SUPPORTED_FIAT return fiat in SUPPORTED_FIAT
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float: def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
""" """
@ -187,6 +184,10 @@ class CryptoToFiatConverter(object):
if not self._is_supported_fiat(fiat=fiat_symbol): if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol)) raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
# No need to convert if both crypto and fiat are the same
if crypto_symbol == fiat_symbol:
return 1.0
if crypto_symbol not in self._cryptomap: if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot) # return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
@ -198,6 +199,6 @@ class CryptoToFiatConverter(object):
convert=fiat_symbol convert=fiat_symbol
)['data']['quotes'][fiat_symbol.upper()]['price'] )['data']['quotes'][fiat_symbol.upper()]['price']
) )
except BaseException as ex: except BaseException as exception:
logger.error("Error in _find_price: %s", ex) logger.error("Error in _find_price: %s", exception)
return 0.0 return 0.0

View File

@ -33,7 +33,7 @@ class FreqtradeBot(object):
This is from here the bot start its logic. This is from here the bot start its logic.
""" """
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None): def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None)-> None:
""" """
Init all variables and object the bot need to work Init all variables and object the bot need to work
:param config: configuration dict, you can use the Configuration.get_config() :param config: configuration dict, you can use the Configuration.get_config()
@ -51,9 +51,9 @@ class FreqtradeBot(object):
# Init objects # Init objects
self.config = config self.config = config
self.analyze = None self.analyze = Analyze(self.config)
self.fiat_converter = None self.fiat_converter = CryptoToFiatConverter()
self.rpc = None self.rpc: RPCManager = RPCManager(self)
self.persistence = None self.persistence = None
self.exchange = None self.exchange = None
@ -66,9 +66,6 @@ class FreqtradeBot(object):
:return: None :return: None
""" """
# Initialize all modules # Initialize all modules
self.analyze = Analyze(self.config)
self.fiat_converter = CryptoToFiatConverter()
self.rpc = RPCManager(self)
persistence.init(self.config, db_url) persistence.init(self.config, db_url)
exchange.init(self.config) exchange.init(self.config)
@ -93,7 +90,7 @@ class FreqtradeBot(object):
persistence.cleanup() persistence.cleanup()
return True return True
def worker(self, old_state: None) -> State: def worker(self, old_state: State = None) -> State:
""" """
Trading routine that must be run at each loop Trading routine that must be run at each loop
:param old_state: the previous service state from the previous call :param old_state: the previous service state from the previous call

View File

@ -13,7 +13,7 @@ def went_down(series: Series) -> bool:
return series < series.shift(1) return series < series.shift(1)
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series): def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
magic = pi * sqrt(2) / smoothing magic = pi * sqrt(2) / smoothing
a1 = exp(-magic) a1 = exp(-magic)
coeff2 = 2 * a1 * cos(magic) coeff2 = 2 * a1 * cos(magic)

View File

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

View File

@ -83,7 +83,7 @@ def file_dump_json(filename, data, is_zip=False) -> None:
json.dump(data, fp, default=str) json.dump(data, fp, default=str)
def format_ms_time(date: str) -> str: def format_ms_time(date: int) -> str:
""" """
convert MS date to readable format. convert MS date to readable format.
: epoch-string in ms : epoch-string in ms

View File

@ -4,8 +4,8 @@ import gzip
import json import json
import logging import logging
import os import os
from typing import Optional, List, Dict, Tuple, Any
import arrow import arrow
from typing import Optional, List, Dict, Tuple
from freqtrade import misc, constants from freqtrade import misc, constants
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
@ -29,7 +29,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -
if stype[0] == 'index': if stype[0] == 'index':
start_index = start start_index = start
elif stype[0] == 'date': elif stype[0] == 'date':
while tickerlist[start_index][0] < start * 1000: while start_index < len(tickerlist) and tickerlist[start_index][0] < start * 1000:
start_index += 1 start_index += 1
if stype[1] == 'line': if stype[1] == 'line':
@ -37,7 +37,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -
if stype[1] == 'index': if stype[1] == 'index':
stop_index = stop stop_index = stop
elif stype[1] == 'date': elif stype[1] == 'date':
while tickerlist[stop_index-1][0] > stop * 1000: while stop_index > 0 and tickerlist[stop_index-1][0] > stop * 1000:
stop_index -= 1 stop_index -= 1
if start_index > stop_index: if start_index > stop_index:
@ -100,15 +100,16 @@ def load_data(datadir: str,
for pair in _pairs: for pair in _pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
if not pairdata: if pairdata:
# download the tickerdata from exchange
download_backtesting_testdata(datadir,
pair=pair,
tick_interval=ticker_interval,
timerange=timerange)
# and retry reading the pair
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
result[pair] = pairdata result[pair] = pairdata
else:
logger.warning(
'No data for pair: "%s", Interval: %s. '
'Use --refresh-pairs-cached to download the data',
pair,
ticker_interval
)
return result return result
@ -143,7 +144,9 @@ def download_pairs(datadir, pairs: List[str],
def load_cached_data_for_updating(filename: str, def load_cached_data_for_updating(filename: str,
tick_interval: str, tick_interval: str,
timerange: Optional[Tuple[Tuple, int, int]]) -> Tuple[list, int]: timerange: Optional[Tuple[Tuple, int, int]]) -> Tuple[
List[Any],
Optional[int]]:
""" """
Load cached data and choose what part of the data should be updated Load cached data and choose what part of the data should be updated
""" """

View File

@ -33,18 +33,6 @@ class Backtesting(object):
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config self.config = config
self.analyze = None
self.ticker_interval = None
self.tickerdata_to_dataframe = None
self.populate_buy_trend = None
self.populate_sell_trend = None
self._init()
def _init(self) -> None:
"""
Init objects required for backtesting
:return: None
"""
self.analyze = Analyze(self.config) self.analyze = Analyze(self.config)
self.ticker_interval = self.analyze.strategy.ticker_interval self.ticker_interval = self.analyze.strategy.ticker_interval
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
@ -78,7 +66,7 @@ class Backtesting(object):
Generates and returns a text table for the given backtest data and the results dataframe Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str :return: pretty printed table with tabulate as str
""" """
stake_currency = self.config.get('stake_currency') stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.8f', '.1f') floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
tabular_data = [] tabular_data = []
@ -106,7 +94,7 @@ class Backtesting(object):
len(results[results.profit_BTC > 0]), len(results[results.profit_BTC > 0]),
len(results[results.profit_BTC < 0]) len(results[results.profit_BTC < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt) return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def _get_sell_trade_entry( def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame, self, pair: str, buy_row: DataFrame,
@ -168,7 +156,7 @@ class Backtesting(object):
record = args.get('record', None) record = args.get('record', None)
records = [] records = []
trades = [] trades = []
trade_count_lock = {} trade_count_lock: Dict = {}
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
@ -230,8 +218,9 @@ class Backtesting(object):
else: else:
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
timerange = Arguments.parse_timerange(self.config.get('timerange')) timerange = Arguments.parse_timerange(None if self.config.get(
data = optimize.load_data( 'timerange') is None else str(self.config.get('timerange')))
data = optimize.load_data( # type: ignore # timerange will be refactored
self.config['datadir'], self.config['datadir'],
pairs=pairs, pairs=pairs,
ticker_interval=self.ticker_interval, ticker_interval=self.ticker_interval,

View File

@ -14,7 +14,7 @@ from argparse import Namespace
from functools import reduce from functools import reduce
from math import exp from math import exp
from operator import itemgetter from operator import itemgetter
from typing import Dict, Any, Callable from typing import Dict, Any, Callable, Optional
import numpy import numpy
import talib.abstract as ta import talib.abstract as ta
@ -60,7 +60,7 @@ class Hyperopt(Backtesting):
self.expected_max_profit = 3.0 self.expected_max_profit = 3.0
# Configuration and data used by hyperopt # Configuration and data used by hyperopt
self.processed = None self.processed: Optional[Dict[str, Any]] = None
# Hyperopt Trials # Hyperopt Trials
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle') self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
@ -344,7 +344,7 @@ class Hyperopt(Backtesting):
""" """
Return the space to use during Hyperopt Return the space to use during Hyperopt
""" """
spaces = {} spaces: Dict = {}
if self.has_space('buy'): if self.has_space('buy'):
spaces = {**spaces, **Hyperopt.indicator_space()} spaces = {**spaces, **Hyperopt.indicator_space()}
if self.has_space('roi'): if self.has_space('roi'):
@ -455,6 +455,7 @@ class Hyperopt(Backtesting):
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration: if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
print('.', end='') print('.', end='')
sys.stdout.flush()
return { return {
'status': STATUS_FAIL, 'status': STATUS_FAIL,
'loss': float('inf') 'loss': float('inf')
@ -479,31 +480,32 @@ class Hyperopt(Backtesting):
'result': result_explanation, 'result': result_explanation,
} }
@staticmethod def format_results(self, results: DataFrame) -> str:
def format_results(results: DataFrame) -> str:
""" """
Return the format result in a string Return the format result in a string
""" """
return ('{:6d} trades. Avg profit {: 5.2f}%. ' return ('{:6d} trades. Avg profit {: 5.2f}%. '
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format( 'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
len(results.index), len(results.index),
results.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(), results.profit_BTC.sum(),
self.config['stake_currency'],
results.profit_percent.sum(), results.profit_percent.sum(),
results.duration.mean(), results.duration.mean(),
) )
def start(self) -> None: def start(self) -> None:
timerange = Arguments.parse_timerange(self.config.get('timerange')) timerange = Arguments.parse_timerange(None if self.config.get(
data = load_data( 'timerange') is None else str(self.config.get('timerange')))
datadir=self.config.get('datadir'), data = load_data( # type: ignore # timerange will be refactored
datadir=str(self.config.get('datadir')),
pairs=self.config['exchange']['pair_whitelist'], pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval, ticker_interval=self.ticker_interval,
timerange=timerange timerange=timerange
) )
if self.has_space('buy'): if self.has_space('buy'):
self.analyze.populate_indicators = Hyperopt.populate_indicators self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
self.processed = self.tickerdata_to_dataframe(data) self.processed = self.tickerdata_to_dataframe(data)
if self.config.get('mongodb'): if self.config.get('mongodb'):

View File

@ -5,7 +5,7 @@ This module contains the class to persist trades into SQLite
import logging import logging
from datetime import datetime from datetime import datetime
from decimal import Decimal, getcontext from decimal import Decimal, getcontext
from typing import Dict, Optional from typing import Dict, Optional, Any
import arrow import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
@ -21,7 +21,7 @@ from sqlalchemy import inspect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_CONF = {} _CONF = {}
_DECL_BASE = declarative_base() _DECL_BASE: Any = declarative_base()
def init(config: dict, engine: Optional[Engine] = None) -> None: def init(config: dict, engine: Optional[Engine] = None) -> None:

View File

@ -2,9 +2,9 @@
This module contains class to define a RPC communications This module contains class to define a RPC communications
""" """
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta, date
from decimal import Decimal from decimal import Decimal
from typing import Tuple, Any from typing import Dict, Tuple, Any
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
@ -114,7 +114,7 @@ class RPC(object):
self, timescale: int, self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]: stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
today = datetime.utcnow().date() today = datetime.utcnow().date()
profit_days = {} profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
return True, '*Daily [n]:* `must be an integer greater than 0`' return True, '*Daily [n]:* `must be an integer greater than 0`'
@ -172,7 +172,7 @@ class RPC(object):
durations = [] durations = []
for trade in trades: for trade in trades:
current_rate = None current_rate: float = 0.0
if not trade.open_rate: if not trade.open_rate:
continue continue
@ -278,7 +278,7 @@ class RPC(object):
value = fiat.convert_amount(total, 'BTC', symbol) value = fiat.convert_amount(total, 'BTC', symbol)
return False, (output, total, symbol, value) return False, (output, total, symbol, value)
def rpc_start(self) -> (bool, str): def rpc_start(self) -> Tuple[bool, str]:
""" """
Handler for start. Handler for start.
""" """
@ -288,7 +288,7 @@ class RPC(object):
self.freqtrade.state = State.RUNNING self.freqtrade.state = State.RUNNING
return False, '`Starting trader ...`' return False, '`Starting trader ...`'
def rpc_stop(self) -> (bool, str): def rpc_stop(self) -> Tuple[bool, str]:
""" """
Handler for stop. Handler for stop.
""" """
@ -316,8 +316,10 @@ class RPC(object):
and order['side'] == 'buy': and order['side'] == 'buy':
exchange.cancel_order(trade.open_order_id, trade.pair) exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close(order.get('price') or trade.open_rate) trade.close(order.get('price') or trade.open_rate)
# TODO: sell amount which has been bought already # Do the best effort, if we don't know 'filled' amount, don't try selling
if order['filled'] is None:
return return
trade.amount = order['filled']
# Ignore trades with an attached LIMIT_SELL order # Ignore trades with an attached LIMIT_SELL order
if order and order['status'] == 'open' \ if order and order['status'] == 'open' \

View File

@ -1,6 +1,7 @@
""" """
This module contains class to manage RPC communications (Telegram, Slack, ...) This module contains class to manage RPC communications (Telegram, Slack, ...)
""" """
from typing import Any, List
import logging import logging
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.telegram import Telegram
@ -21,8 +22,8 @@ class RPCManager(object):
""" """
self.freqtrade = freqtrade self.freqtrade = freqtrade
self.registered_modules = [] self.registered_modules: List[str] = []
self.telegram = None self.telegram: Any = None
self._init() self._init()
def _init(self) -> None: def _init(self) -> None:

View File

@ -18,7 +18,7 @@ from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]: def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
""" """
Decorator to check if the message comes from the correct chat_id Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler :param command_handler: Telegram CommandHandler
@ -65,7 +65,7 @@ class Telegram(RPC):
""" """
super().__init__(freqtrade) super().__init__(freqtrade)
self._updater = None self._updater: Updater = None
self._config = freqtrade.config self._config = freqtrade.config
self._init() self._init()

View File

@ -2,7 +2,7 @@
IStrategy interface IStrategy interface
This module defines the interface to apply for strategies This module defines the interface to apply for strategies
""" """
from typing import Dict
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pandas import DataFrame from pandas import DataFrame
@ -16,9 +16,13 @@ class IStrategy(ABC):
Attributes you can use: Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> int: value of the ticker interval to use for the strategy ticker_interval -> str: value of the ticker interval to use for the strategy
""" """
minimal_roi: Dict
stoploss: float
ticker_interval: str
@abstractmethod @abstractmethod
def populate_indicators(self, dataframe: DataFrame) -> DataFrame: def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
""" """

View File

@ -33,7 +33,8 @@ class StrategyResolver(object):
# Verify the strategy is in the configuration, otherwise fallback to the default strategy # Verify the strategy is in the configuration, otherwise fallback to the default strategy
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path')) self.strategy: IStrategy = self._load_strategy(strategy_name,
extra_dir=config.get('strategy_path'))
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
@ -61,7 +62,7 @@ class StrategyResolver(object):
self.strategy.stoploss = float(self.strategy.stoploss) self.strategy.stoploss = float(self.strategy.stoploss)
def _load_strategy( def _load_strategy(
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]: self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
""" """
Search and loads the specified strategy. Search and loads the specified strategy.
:param strategy_name: name of the module to import :param strategy_name: name of the module to import
@ -101,7 +102,7 @@ class StrategyResolver(object):
# Generate spec based on absolute path # Generate spec based on absolute path
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path) spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
valid_strategies_gen = ( valid_strategies_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass) obj for name, obj in inspect.getmembers(module, inspect.isclass)

View File

@ -393,6 +393,78 @@ def test_get_ticker_history(default_conf, mocker):
get_ticker_history('EFGH/BTC', default_conf['ticker_interval']) get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
def test_get_ticker_history_sort(default_conf, mocker):
api_mock = MagicMock()
# GDAX use-case (real data from GDAX)
# This ticker history is ordered DESC (newest first, oldest last)
tick = [
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
[1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186],
[1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136],
[1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521],
[1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753],
[1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999],
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
]
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)
# Test the ticker history sort
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651
assert ticks[0][3] == 0.07649
assert ticks[0][4] == 0.07651
assert ticks[0][5] == 2.5734867
assert ticks[9][0] == 1527833100000
assert ticks[9][1] == 0.07666
assert ticks[9][2] == 0.07671
assert ticks[9][3] == 0.07666
assert ticks[9][4] == 0.07668
assert ticks[9][5] == 16.65244264
# Bittrex use-case (real data from Bittrex)
# This ticker history is ordered ASC (oldest first, newest last)
tick = [
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
[1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773],
[1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565],
[1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326],
[1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831],
[1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884],
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
]
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)
# Test the ticker history sort
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766
assert ticks[0][3] == 0.07627
assert ticks[0][4] == 0.07657998
assert ticks[0][5] == 1.85216924
assert ticks[9][0] == 1527830400000
assert ticks[9][1] == 0.07671
assert ticks[9][2] == 0.07674399
assert ticks[9][3] == 0.07629216
assert ticks[9][4] == 0.07655213
assert ticks[9][5] == 2.31452783
def test_cancel_order_dry_run(default_conf, mocker): def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)

View File

@ -182,7 +182,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']), 'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -230,7 +230,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']), 'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -309,23 +309,6 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
assert start_mock.call_count == 1 assert start_mock.call_count == 1
def test_backtesting__init__(mocker, default_conf) -> None:
"""
Test Backtesting.__init__() method
"""
init_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert backtesting.analyze is None
assert backtesting.ticker_interval is None
assert backtesting.tickerdata_to_dataframe is None
assert backtesting.populate_buy_trend is None
assert backtesting.populate_sell_trend is None
assert init_mock.call_count == 1
def test_backtesting_init(mocker, default_conf) -> None: def test_backtesting_init(mocker, default_conf) -> None:
""" """
Test Backtesting._init() method Test Backtesting._init() method
@ -397,16 +380,15 @@ def test_generate_text_table(default_conf, mocker):
) )
result_str = ( result_str = (
'pair buy count avg profit % ' '| pair | buy count | avg profit % | '
'total profit BTC avg duration profit loss\n' 'total profit BTC | avg duration | profit | loss |\n'
'------- ----------- -------------- ' '|:--------|------------:|---------------:|'
'------------------ -------------- -------- ------\n' '-------------------:|---------------:|---------:|-------:|\n'
'ETH/BTC 2 15.00 ' '| ETH/BTC | 2 | 15.00 | '
'0.60000000 20.0 2 0\n' '0.60000000 | 20.0 | 2 | 0 |\n'
'TOTAL 2 15.00 ' '| TOTAL | 2 | 15.00 | '
'0.60000000 20.0 2 0' '0.60000000 | 20.0 | 2 | 0 |'
) )
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
@ -655,7 +637,7 @@ def test_backtest_start_live(default_conf, mocker, caplog):
'Parameter -l/--live detected ...', 'Parameter -l/--live detected ...',
'Using max_open_trades: 1 ...', 'Using max_open_trades: 1 ...',
'Parameter --timerange detected: -100 ..', 'Parameter --timerange detected: -100 ..',
'Parameter --datadir detected: freqtrade/tests/testdata ...', 'Using data folder: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...', 'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...', 'Using stake_amount: 0.001 ...',
'Downloading data for all pairs in whitelist ...', 'Downloading data for all pairs in whitelist ...',

View File

@ -389,10 +389,12 @@ def test_start_uses_mongotrials(mocker, init_hyperopt, default_conf) -> None:
# test buy_strategy_generator def populate_buy_trend # test buy_strategy_generator def populate_buy_trend
# test optimizer if 'ro_t1' in params # test optimizer if 'ro_t1' in params
def test_format_results(): def test_format_results(init_hyperopt):
""" """
Test Hyperopt.format_results() Test Hyperopt.format_results()
""" """
# Test with BTC as stake_currency
trades = [ trades = [
('ETH/BTC', 2, 2, 123), ('ETH/BTC', 2, 2, 123),
('LTC/BTC', 1, 1, 123), ('LTC/BTC', 1, 1, 123),
@ -400,8 +402,21 @@ def test_format_results():
] ]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
df = pd.DataFrame.from_records(trades, columns=labels) df = pd.DataFrame.from_records(trades, columns=labels)
x = Hyperopt.format_results(df)
assert x.find(' 66.67%') result = _HYPEROPT.format_results(df)
assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %')
# Test with EUR as stake_currency
trades = [
('ETH/EUR', 2, 2, 123),
('LTC/EUR', 1, 1, 123),
('XPR/EUR', -1, -2, -246)
]
df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df)
assert result.find('Total profit 1.00000000 EUR')
def test_signal_handler(mocker, init_hyperopt): def test_signal_handler(mocker, init_hyperopt):

View File

@ -99,7 +99,21 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
_backup_file(file) _backup_file(file)
optimize.load_data(None, ticker_interval='1m', pairs=['MEME/BTC']) # 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", Interval: 1m. '
'Use --refresh-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 os.path.isfile(file) is True
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples) assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file) _clean_test_file(file)

View File

@ -449,20 +449,44 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
# make an limit-buy open trade # make an limit-buy open trade
trade = Trade.query.filter(Trade.id == '1').first()
filled_amount = trade.amount / 2
mocker.patch( mocker.patch(
'freqtrade.freqtradebot.exchange.get_order', 'freqtrade.freqtradebot.exchange.get_order',
return_value={ return_value={
'status': 'open', 'status': 'open',
'type': 'limit', 'type': 'limit',
'side': 'buy' 'side': 'buy',
'filled': filled_amount
} }
) )
# check that the trade is called, which is done # check that the trade is called, which is done by ensuring exchange.cancel_order is called
# by ensuring exchange.cancel_order is called # and trade amount is updated
(error, res) = rpc.rpc_forcesell('1') (error, res) = rpc.rpc_forcesell('1')
assert not error assert not error
assert res == '' assert res == ''
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount
freqtradebot.create_trade()
trade = Trade.query.filter(Trade.id == '2').first()
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': None
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('2')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 2
assert trade.amount == amount
freqtradebot.create_trade() freqtradebot.create_trade()
# make an limit-sell open trade # make an limit-sell open trade
@ -474,11 +498,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
'side': 'sell' 'side': 'sell'
} }
) )
(error, res) = rpc.rpc_forcesell('2') (error, res) = rpc.rpc_forcesell('3')
assert not error assert not error
assert res == '' assert res == ''
# status quo, no exchange calls # status quo, no exchange calls
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 2
def test_performance_handle(default_conf, ticker, limit_buy_order, fee, def test_performance_handle(default_conf, ticker, limit_buy_order, fee,

View File

@ -116,6 +116,12 @@ def test_parse_timerange_incorrect() -> None:
timerange = Arguments.parse_timerange('20100522-20150730') timerange = Arguments.parse_timerange('20100522-20150730')
assert timerange == (('date', 'date'), 1274486400, 1438214400) assert timerange == (('date', 'date'), 1274486400, 1438214400)
# Added test for unix timestamp - BTC genesis date
assert (('date', None), 1231006505, None) == Arguments.parse_timerange('1231006505-')
assert ((None, 'date'), None, 1233360000) == Arguments.parse_timerange('-1233360000')
timerange = Arguments.parse_timerange('1231006505-1233360000')
assert timerange == (('date', 'date'), 1231006505, 1233360000)
with pytest.raises(Exception, match=r'Incorrect syntax.*'): with pytest.raises(Exception, match=r'Incorrect syntax.*'):
Arguments.parse_timerange('-') Arguments.parse_timerange('-')

View File

@ -6,6 +6,7 @@ Unit test file for configuration.py
import json import json
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from argparse import Namespace
import pytest import pytest
from jsonschema import ValidationError from jsonschema import ValidationError
@ -37,7 +38,7 @@ def test_load_config_invalid_pair(default_conf) -> None:
conf['exchange']['pair_whitelist'].append('ETH-BTC') conf['exchange']['pair_whitelist'].append('ETH-BTC')
with pytest.raises(ValidationError, match=r'.*does not match.*'): with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration([]) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(conf)
@ -49,7 +50,7 @@ def test_load_config_missing_attributes(default_conf) -> None:
conf.pop('exchange') conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
configuration = Configuration([]) configuration = Configuration(Namespace())
configuration._validate_config(conf) configuration._validate_config(conf)
@ -73,7 +74,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
configuration = Configuration([]) configuration = Configuration(Namespace())
validated_conf = configuration._load_config_file('somefile') validated_conf = configuration._load_config_file('somefile')
assert file_mock.call_count == 1 assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items() assert validated_conf.items() >= default_conf.items()
@ -91,7 +92,7 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
read_data=json.dumps(conf) read_data=json.dumps(conf)
)) ))
Configuration([])._load_config_file('somefile') Configuration(Namespace())._load_config_file('somefile')
assert file_mock.call_count == 1 assert file_mock.call_count == 1
assert log_has('Validating configuration ...', caplog.record_tuples) assert log_has('Validating configuration ...', caplog.record_tuples)
@ -104,7 +105,7 @@ def test_load_config_file_exception(mocker, caplog) -> None:
'freqtrade.configuration.open', 'freqtrade.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found')) MagicMock(side_effect=FileNotFoundError('File not found'))
) )
configuration = Configuration([]) configuration = Configuration(Namespace())
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
configuration._load_config_file('somefile') configuration._load_config_file('somefile')
@ -140,13 +141,13 @@ def test_load_config_with_params(default_conf, mocker) -> None:
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
args = [ arglist = [
'--dynamic-whitelist', '10', '--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--strategy-path', '/some/path', '--strategy-path', '/some/path',
'--dry-run-db', '--dry-run-db',
] ]
args = Arguments(args, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
validated_conf = configuration.load_config() validated_conf = configuration.load_config()
@ -186,12 +187,12 @@ def test_show_info(default_conf, mocker, caplog) -> None:
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
args = [ arglist = [
'--dynamic-whitelist', '10', '--dynamic-whitelist', '10',
'--strategy', 'TestStrategy', '--strategy', 'TestStrategy',
'--dry-run-db' '--dry-run-db'
] ]
args = Arguments(args, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
configuration.get_config() configuration.get_config()
@ -214,8 +215,8 @@ def test_show_info(default_conf, mocker, caplog) -> None:
) )
# Test the Dry run condition # Test the Dry run condition
configuration.config.update({'dry_run': False}) configuration.config.update({'dry_run': False}) # type: ignore
configuration._load_common_config(configuration.config) configuration._load_common_config(configuration.config) # type: ignore
assert log_has( assert log_has(
'Dry run is disabled. (--dry_run_db ignored)', 'Dry run is disabled. (--dry_run_db ignored)',
caplog.record_tuples caplog.record_tuples
@ -230,13 +231,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
args = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'backtesting' 'backtesting'
] ]
args = Arguments(args, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -247,7 +248,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']), 'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -274,7 +275,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
args = [ arglist = [
'--config', 'config.json', '--config', 'config.json',
'--strategy', 'DefaultStrategy', '--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar', '--datadir', '/foo/bar',
@ -287,7 +288,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--export', '/bar/foo' '--export', '/bar/foo'
] ]
args = Arguments(args, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -298,7 +299,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange'] assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config assert 'datadir' in config
assert log_has( assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']), 'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples caplog.record_tuples
) )
assert 'ticker_interval' in config assert 'ticker_interval' in config
@ -338,14 +339,14 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
read_data=json.dumps(default_conf) read_data=json.dumps(default_conf)
)) ))
args = [ arglist = [
'hyperopt', 'hyperopt',
'--epochs', '10', '--epochs', '10',
'--use-mongodb', '--use-mongodb',
'--spaces', 'all', '--spaces', 'all',
] ]
args = Arguments(args, '').get_parsed_arg() args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args) configuration = Configuration(args)
config = configuration.get_config() config = configuration.get_config()
@ -369,7 +370,7 @@ def test_check_exchange(default_conf) -> None:
Test the configuration validator with a missing attribute Test the configuration validator with a missing attribute
""" """
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
configuration = Configuration([]) configuration = Configuration(Namespace())
# Test a valid exchange # Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'}) conf.get('exchange').update({'name': 'BITTREX'})

View File

@ -6,6 +6,8 @@ from unittest.mock import MagicMock
import pytest import pytest
from requests.exceptions import RequestException
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
from freqtrade.tests.conftest import patch_coinmarketcap from freqtrade.tests.conftest import patch_coinmarketcap
@ -124,6 +126,20 @@ def test_fiat_convert_get_price(mocker):
assert fiat_convert._pairs[0]._expiration is not expiration assert fiat_convert._pairs[0]._expiration is not expiration
def test_fiat_convert_same_currencies(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
def test_fiat_convert_two_FIAT(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
def test_loadcryptomap(mocker): def test_loadcryptomap(mocker):
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
@ -133,6 +149,21 @@ def test_loadcryptomap(mocker):
assert fiat_convert._cryptomap["BTC"] == "1" 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(): def test_fiat_convert_without_network():
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap

View File

@ -1,5 +1,5 @@
ccxt==1.14.27 ccxt==1.14.121
SQLAlchemy==1.2.7 SQLAlchemy==1.2.8
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0
@ -12,7 +12,7 @@ scipy==1.1.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.14.3 numpy==1.14.3
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.5.1 pytest==3.6.0
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest-cov==2.5.1 pytest-cov==2.5.1
hyperopt==0.1 hyperopt==0.1

View File

@ -2,3 +2,6 @@
#ignore = #ignore =
max-line-length = 100 max-line-length = 100
max-complexity = 12 max-complexity = 12
[mypy]
ignore_missing_imports = True

View File

@ -2,16 +2,17 @@
#encoding=utf8 #encoding=utf8
function updateenv () { function updateenv () {
echo " echo "-------------------------"
------------------------- echo "Update your virtual env"
Update your virtual env echo "-------------------------"
-------------------------
"
source .env/bin/activate source .env/bin/activate
pip3.6 install --upgrade pip echo "pip3 install in-progress. Please wait..."
pip3 install -r requirements.txt --upgrade pip3.6 install --quiet --upgrade pip
pip3 install -r requirements.txt pip3 install --quiet -r requirements.txt --upgrade
pip3 install -e . pip3 install --quiet -r requirements.txt
pip3 install --quiet -e .
echo "pip3 install completed"
echo
} }
# Install tab lib # Install tab lib
@ -29,10 +30,11 @@ function install_macos () {
echo "-------------------------" echo "-------------------------"
echo "Install Brew" echo "Install Brew"
echo "-------------------------" echo "-------------------------"
echo
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
fi fi
brew install python3 wget ta-lib brew install python3 wget ta-lib
test_and_fix_python_on_mac
} }
# Install bot Debian_ubuntu # Install bot Debian_ubuntu
@ -54,7 +56,6 @@ function reset () {
echo "----------------------------" echo "----------------------------"
echo "Reset branch and virtual env" echo "Reset branch and virtual env"
echo "----------------------------" echo "----------------------------"
echo
if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ] if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ]
then then
if [ -d ".env" ]; then if [ -d ".env" ]; then
@ -77,34 +78,53 @@ function reset () {
echo "Reset ignored because you are not on 'master' or 'develop'." echo "Reset ignored because you are not on 'master' or 'develop'."
fi fi
echo
python3.6 -m venv .env python3.6 -m venv .env
updateenv updateenv
} }
function test_and_fix_python_on_mac() {
if ! [ -x "$(command -v python3.6)" ]
then
echo "-------------------------"
echo "Fixing Python"
echo "-------------------------"
echo "Python 3.6 is not linked in your system. Fixing it..."
brew link --overwrite python
echo
fi
}
function config_generator () { function config_generator () {
echo "Starting to generate config.json" echo "Starting to generate config.json"
echo
echo "-------------------------"
echo "General configuration" echo "General configuration"
echo "-------------------------" echo "-------------------------"
default_max_trades=3
read -p "Max open trades: (Default: $default_max_trades) " max_trades
max_trades=${max_trades:-$default_max_trades}
default_stake_amount=0.05
read -p "Stake amount: (Default: $default_stake_amount) " stake_amount
stake_amount=${stake_amount:-$default_stake_amount}
default_stake_currency="BTC"
read -p "Stake currency: (Default: $default_stake_currency) " stake_currency
stake_currency=${stake_currency:-$default_stake_currency}
default_fiat_currency="USD"
read -p "Fiat currency: (Default: $default_fiat_currency) " fiat_currency
fiat_currency=${fiat_currency:-$default_fiat_currency}
echo echo
read -p "Max open trades: (Default: 3) " max_trades echo "Exchange config generator"
read -p "Stake amount: (Default: 0.05) " stake_amount
read -p "Stake currency: (Default: BTC) " stake_currency
read -p "Fiat currency: (Default: USD) " fiat_currency
echo "------------------------" echo "------------------------"
echo "Bittrex config generator"
echo "------------------------"
echo
read -p "Exchange API key: " api_key read -p "Exchange API key: " api_key
read -p "Exchange API Secret: " api_secret read -p "Exchange API Secret: " api_secret
echo "-------------------------" echo
echo "Telegram config generator" echo "Telegram config generator"
echo "-------------------------" echo "-------------------------"
read -p "Telegram Token: " token read -p "Telegram Token: " token
@ -123,6 +143,10 @@ function config_generator () {
} }
function config () { function config () {
echo "-------------------------"
echo "Config file generator"
echo "-------------------------"
if [ -f config.json ] if [ -f config.json ]
then then
read -p "A config file already exist, do you want to override it [Y/N]? " read -p "A config file already exist, do you want to override it [Y/N]? "
@ -136,22 +160,26 @@ function config () {
config_generator config_generator
fi fi
echo
echo "-------------------------"
echo "Config file generated"
echo "-------------------------"
echo "Edit ./config.json to modify Pair and other configurations." echo "Edit ./config.json to modify Pair and other configurations."
echo
} }
function install () { function install () {
echo "-------------------------" echo "-------------------------"
echo "Install mandatory dependencies" echo "Install mandatory dependencies"
echo "-------------------------" echo "-------------------------"
echo
if [ "$(uname -s)" == "Darwin" ] if [ "$(uname -s)" == "Darwin" ]
then then
echo "- You are on macOS" echo "macOS detected. Setup for this system in-progress"
install_macos install_macos
elif [ -x "$(command -v apt-get)" ] elif [ -x "$(command -v apt-get)" ]
then then
echo "- You are on Debian/Ubuntu" echo "Debian/Ubuntu detected. Setup for this system in-progress"
install_debian install_debian
else else
echo "This script does not support your OS." echo "This script does not support your OS."
@ -159,12 +187,13 @@ function install () {
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell." echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
sleep 10 sleep 10
fi fi
echo
reset reset
echo "
- Install complete.
"
config config
echo "You can now use the bot by executing 'source .env/bin/activate; python3 freqtrade/main.py'." echo "-------------------------"
echo "Run the bot"
echo "-------------------------"
echo "You can now use the bot by executing 'source .env/bin/activate; python3.6 freqtrade/main.py'."
} }
function plot () { function plot () {