Merge branch 'develop' into self_building_ticker_history
This commit is contained in:
commit
fbc09e05c4
14
.gitignore
vendored
14
.gitignore
vendored
@ -1,3 +1,11 @@
|
|||||||
|
# Freqtrade rules
|
||||||
|
freqtrade/tests/testdata/*.json
|
||||||
|
hyperopt_conf.py
|
||||||
|
config.json
|
||||||
|
*.sqlite
|
||||||
|
.hyperopt
|
||||||
|
logfile.txt
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
@ -73,12 +81,6 @@ target/
|
|||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
config.json
|
|
||||||
preprocessor.py
|
|
||||||
*.sqlite
|
|
||||||
.hyperopt
|
|
||||||
logfile.txt
|
|
||||||
|
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
.idea
|
.idea
|
||||||
|
25
.travis.yml
25
.travis.yml
@ -1,4 +1,4 @@
|
|||||||
sudo: false
|
sudo: true
|
||||||
os:
|
os:
|
||||||
- linux
|
- linux
|
||||||
language: python
|
language: python
|
||||||
@ -11,16 +11,27 @@ addons:
|
|||||||
- libdw-dev
|
- libdw-dev
|
||||||
- binutils-dev
|
- binutils-dev
|
||||||
install:
|
install:
|
||||||
- wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
- ./install_ta-lib.sh
|
||||||
- tar zxvf ta-lib-0.4.0-src.tar.gz
|
|
||||||
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
|
|
||||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
- pip install coveralls
|
- pip install flake8 coveralls
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
script:
|
- pip install -e .
|
||||||
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
jobs:
|
||||||
|
include:
|
||||||
|
- script: pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||||
|
- script:
|
||||||
|
- cp config.json.example config.json
|
||||||
|
- python freqtrade/main.py backtesting
|
||||||
|
- script:
|
||||||
|
- cp config.json.example config.json
|
||||||
|
- python freqtrade/main.py hyperopt -e 5
|
||||||
|
- script: flake8 freqtrade
|
||||||
after_success:
|
after_success:
|
||||||
- coveralls
|
- coveralls
|
||||||
notifications:
|
notifications:
|
||||||
slack:
|
slack:
|
||||||
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.cache/pip
|
||||||
|
- ta-lib
|
39
CONTRIBUTING.md
Normal file
39
CONTRIBUTING.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
||||||
|
|
||||||
|
- Create your PR against the `develop` branch, not `master`.
|
||||||
|
- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100).
|
||||||
|
|
||||||
|
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE)
|
||||||
|
or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
|
||||||
|
|
||||||
|
Before sending the PR:
|
||||||
|
|
||||||
|
## Run unit tests
|
||||||
|
|
||||||
|
All unit tests must pass. If a unit test is broken, change your code to make it pass. It means you have introduced a regression
|
||||||
|
|
||||||
|
**Test the whole project**
|
||||||
|
```bash
|
||||||
|
pytest freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test only one file**
|
||||||
|
```bash
|
||||||
|
pytest freqtrade/tests/test_<file_name>.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test only one method from one file**
|
||||||
|
```bash
|
||||||
|
pytest freqtrade/tests/test_<file_name>.py:test_<method_name>
|
||||||
|
```
|
||||||
|
## Test if your code is PEP8 compliant
|
||||||
|
**Install packages** (If not already installed)
|
||||||
|
```bash
|
||||||
|
pip3.6 install flake8 coveralls
|
||||||
|
```
|
||||||
|
**Run Flake8**
|
||||||
|
```bash
|
||||||
|
flake8 freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
|
@ -20,4 +20,4 @@ RUN pip install -r requirements.txt
|
|||||||
# Install and execute
|
# Install and execute
|
||||||
COPY . /freqtrade/
|
COPY . /freqtrade/
|
||||||
RUN pip install -e .
|
RUN pip install -e .
|
||||||
CMD ["freqtrade"]
|
ENTRYPOINT ["freqtrade"]
|
||||||
|
80
README.md
80
README.md
@ -25,6 +25,7 @@ Persistence is achieved through sqlite.
|
|||||||
* /forcesell <trade_id>|all: Instantly sells the given trade (Ignoring `minimum_roi`).
|
* /forcesell <trade_id>|all: Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
* /performance: Show performance of each finished trade grouped by pair
|
* /performance: Show performance of each finished trade grouped by pair
|
||||||
* /balance: Show account balance per currency
|
* /balance: Show account balance per currency
|
||||||
|
* /daily <n>: Shows profit or loss per day, over the last n days
|
||||||
* /help: Show help message
|
* /help: Show help message
|
||||||
* /version: Show version
|
* /version: Show version
|
||||||
|
|
||||||
@ -54,6 +55,12 @@ 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.
|
||||||
|
|
||||||
|
`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 other values should be self-explanatory,
|
The other values should be self-explanatory,
|
||||||
if not feel free to raise a github issue.
|
if not feel free to raise a github issue.
|
||||||
|
|
||||||
@ -61,6 +68,7 @@ if not feel free to raise a github issue.
|
|||||||
* python3.6
|
* python3.6
|
||||||
* sqlite
|
* sqlite
|
||||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||||
|
* Minimal (advised) system requirements: 2GB RAM, 1GB data, 2vCPU
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
@ -137,16 +145,26 @@ $ docker start freqtrade
|
|||||||
You do not need to rebuild the image for configuration
|
You do not need to rebuild the image for configuration
|
||||||
changes, it will suffice to edit `config.json` and restart the container.
|
changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
|
#### systemd service file
|
||||||
|
Copy `./freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`)
|
||||||
|
and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
After that you can start the daemon with:
|
||||||
|
```bash
|
||||||
|
$ systemctl --user start freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
```
|
```
|
||||||
usage: freqtrade [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist]
|
usage: main.py [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist [INT]]
|
||||||
{backtesting} ...
|
[--dry-run-db]
|
||||||
|
{backtesting,hyperopt} ...
|
||||||
|
|
||||||
Simple High Frequency Trading Bot for crypto currencies
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
{backtesting}
|
{backtesting,hyperopt}
|
||||||
backtesting backtesting module
|
backtesting backtesting module
|
||||||
|
hyperopt hyperopt module
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -154,17 +172,42 @@ optional arguments:
|
|||||||
specify configuration file (default: config.json)
|
specify configuration file (default: config.json)
|
||||||
-v, --verbose be verbose
|
-v, --verbose be verbose
|
||||||
--version show program's version number and exit
|
--version show program's version number and exit
|
||||||
--dynamic-whitelist dynamically generate and update whitelist based on 24h
|
--dynamic-whitelist [INT]
|
||||||
BaseVolume
|
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.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Dynamic whitelist example
|
||||||
|
Per default `--dynamic-whitelist` will retrieve the 20 currencies based
|
||||||
|
on BaseVolume. This value can be changed when you run the script.
|
||||||
|
|
||||||
|
**By Default**
|
||||||
|
Get the 20 currencies based on BaseVolume.
|
||||||
|
```bash
|
||||||
|
freqtrade --dynamic-whitelist
|
||||||
|
```
|
||||||
|
|
||||||
|
**Customize the number of currencies to retrieve**
|
||||||
|
Get the 30 currencies based on BaseVolume.
|
||||||
|
```bash
|
||||||
|
freqtrade --dynamic-whitelist 30
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exception**
|
||||||
|
`--dynamic-whitelist` must be greater than 0. If you enter 0 or a
|
||||||
|
negative value (e.g -2), `--dynamic-whitelist` will use the default
|
||||||
|
value (20).
|
||||||
|
|
||||||
### Backtesting
|
### Backtesting
|
||||||
|
|
||||||
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: freqtrade backtesting [-h] [-l] [-i INT] [--realistic-simulation]
|
||||||
|
[-r]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -174,9 +217,25 @@ optional arguments:
|
|||||||
--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
|
||||||
|
-r, --refresh-pairs-cached
|
||||||
|
refresh the pairs files in tests/testdata with
|
||||||
|
the latest data from Bittrex. Use it if you want
|
||||||
|
to run your backtesting with up-to-date data.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### How to use --refresh-pairs-cached parameter?
|
||||||
|
The first time your run Backtesting, it will take the pairs your have
|
||||||
|
set in your config file and download data from Bittrex.
|
||||||
|
|
||||||
|
If for any reason you want to update your data set, you use
|
||||||
|
`--refresh-pairs-cached` to force Backtesting to update the data it has.
|
||||||
|
**Use it only if you want to update your data set. You will not be able
|
||||||
|
to come back to the previous version.**
|
||||||
|
|
||||||
|
To test your strategy with latest data, we recommend to continue using
|
||||||
|
the parameter `-l` or `--live`.
|
||||||
|
|
||||||
|
|
||||||
### Hyperopt
|
### Hyperopt
|
||||||
|
|
||||||
It is possible to use hyperopt for trading strategy optimization.
|
It is possible to use hyperopt for trading strategy optimization.
|
||||||
@ -201,8 +260,5 @@ $ pytest freqtrade
|
|||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
We welcome contributions. See our [contribution guide](https://github.com/gcarq/freqtrade/blob/develop/README.md)
|
||||||
|
for more details.
|
||||||
- Create your PR against the `develop` branch, not `master`.
|
|
||||||
- New features need to contain unit tests.
|
|
||||||
- If you are unsure, discuss the feature on [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) or in a [issue](https://github.com/gcarq/freqtrade/issues) before a PR.
|
|
@ -2,6 +2,7 @@
|
|||||||
"max_open_trades": 3,
|
"max_open_trades": 3,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
|
"fiat_display_currency": "USD",
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
@ -18,14 +19,16 @@
|
|||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"BTC_RLC",
|
"BTC_ETH",
|
||||||
"BTC_TKN",
|
"BTC_LTC",
|
||||||
"BTC_TRST",
|
"BTC_ETC",
|
||||||
"BTC_SWT",
|
"BTC_DASH",
|
||||||
"BTC_PIVX",
|
"BTC_ZEC",
|
||||||
"BTC_MLN",
|
"BTC_XLM",
|
||||||
"BTC_XZC",
|
"BTC_NXT",
|
||||||
"BTC_LUN"
|
"BTC_POWR",
|
||||||
|
"BTC_ADA",
|
||||||
|
"BTC_XMR"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
|
14
freqtrade.service
Normal file
14
freqtrade.service
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Freqtrade Daemon
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# Set WorkingDirectory and ExecStart to your file paths accordingly
|
||||||
|
# NOTE: %h will be resolved to /home/<username>
|
||||||
|
WorkingDirectory=%h/freqtrade
|
||||||
|
ExecStart=/usr/bin/freqtrade --dynamic-whitelist 40
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
|
@ -144,6 +144,9 @@ def get_signal(pair: str, signal: SignalType) -> bool:
|
|||||||
except ValueError as ex:
|
except ValueError as ex:
|
||||||
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
|
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
|
||||||
return False
|
return False
|
||||||
|
except Exception:
|
||||||
|
logger.exception('Unexpected error when analyzing ticker for pair %s.', pair)
|
||||||
|
return False
|
||||||
|
|
||||||
if dataframe.empty:
|
if dataframe.empty:
|
||||||
return False
|
return False
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from random import randint
|
from random import randint
|
||||||
|
from threading import RLock
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
@ -14,6 +15,7 @@ from freqtrade.exchange.bittrex import Bittrex
|
|||||||
from freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
lock = RLock()
|
||||||
|
|
||||||
# Current selected exchange
|
# Current selected exchange
|
||||||
_API: Exchange = None
|
_API: Exchange = None
|
||||||
@ -138,7 +140,7 @@ def get_ticker(pair: str) -> dict:
|
|||||||
return _API.get_ticker(pair)
|
return _API.get_ticker(pair)
|
||||||
|
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=100, ttl=30))
|
@cached(TTLCache(maxsize=100, ttl=30), lock=lock)
|
||||||
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
|
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
|
||||||
return _API.get_ticker_history(pair, tick_interval)
|
return _API.get_ticker_history(pair, tick_interval)
|
||||||
|
|
||||||
|
@ -40,46 +40,68 @@ class Bittrex(Exchange):
|
|||||||
api_version=API_V2_0,
|
api_version=API_V2_0,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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('Got {}'.format(response['message']))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fee(self) -> float:
|
def fee(self) -> float:
|
||||||
# See https://bittrex.com/fees
|
# 0.25 %: See https://bittrex.com/fees
|
||||||
return 0.0025
|
return 0.0025
|
||||||
|
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise OperationalException(
|
Bittrex._validate_response(data)
|
||||||
'{message} params=({pair}, {rate}, {amount})'.format(
|
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
||||||
message=data['message'], pair=pair, rate=rate, amount=amount))
|
message=data['message'],
|
||||||
|
pair=pair,
|
||||||
|
rate=rate,
|
||||||
|
amount=amount))
|
||||||
return data['result']['uuid']
|
return data['result']['uuid']
|
||||||
|
|
||||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise OperationalException(
|
Bittrex._validate_response(data)
|
||||||
'{message} params=({pair}, {rate}, {amount})'.format(
|
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
|
||||||
message=data['message'], pair=pair, rate=rate, amount=amount))
|
message=data['message'],
|
||||||
|
pair=pair,
|
||||||
|
rate=rate,
|
||||||
|
amount=amount))
|
||||||
return data['result']['uuid']
|
return data['result']['uuid']
|
||||||
|
|
||||||
def get_balance(self, currency: str) -> float:
|
def get_balance(self, currency: str) -> float:
|
||||||
data = _API.get_balance(currency)
|
data = _API.get_balance(currency)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message} params=({currency})'.format(
|
raise OperationalException('{message} params=({currency})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
currency=currency))
|
currency=currency))
|
||||||
return float(data['result']['Balance'] or 0.0)
|
return float(data['result']['Balance'] or 0.0)
|
||||||
|
|
||||||
|
|
||||||
def get_balances(self):
|
def get_balances(self):
|
||||||
data = _API.get_balances()
|
data = _API.get_balances()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise OperationalException(
|
Bittrex._validate_response(data)
|
||||||
'{message}'.format(
|
raise OperationalException('{message}'.format(message=data['message'])
|
||||||
message=data['message']))
|
|
||||||
return data['result']
|
return data['result']
|
||||||
|
|
||||||
|
|
||||||
def get_ticker(self, pair: str) -> dict:
|
def get_ticker(self, pair: str) -> dict:
|
||||||
data = _API.get_ticker(pair.replace('_', '-'))
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message} params=({pair})'.format(
|
raise OperationalException('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair))
|
pair=pair))
|
||||||
@ -145,6 +167,7 @@ class Bittrex(Exchange):
|
|||||||
pair=pair))
|
pair=pair))
|
||||||
|
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message} params=({pair})'.format(
|
raise OperationalException('{message} params=({pair})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
pair=pair))
|
pair=pair))
|
||||||
@ -154,6 +177,7 @@ class Bittrex(Exchange):
|
|||||||
def get_order(self, order_id: str) -> Dict:
|
def get_order(self, order_id: str) -> Dict:
|
||||||
data = _API.get_order(order_id)
|
data = _API.get_order(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message} params=({order_id})'.format(
|
raise OperationalException('{message} params=({order_id})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
order_id=order_id))
|
order_id=order_id))
|
||||||
@ -172,6 +196,7 @@ class Bittrex(Exchange):
|
|||||||
def cancel_order(self, order_id: str) -> None:
|
def cancel_order(self, order_id: str) -> None:
|
||||||
data = _API.cancel(order_id)
|
data = _API.cancel(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
|
Bittrex._validate_response(data)
|
||||||
raise OperationalException('{message} params=({order_id})'.format(
|
raise OperationalException('{message} params=({order_id})'.format(
|
||||||
message=data['message'],
|
message=data['message'],
|
||||||
order_id=order_id))
|
order_id=order_id))
|
||||||
@ -183,25 +208,22 @@ class Bittrex(Exchange):
|
|||||||
def get_markets(self) -> List[str]:
|
def get_markets(self) -> List[str]:
|
||||||
data = _API.get_markets()
|
data = _API.get_markets()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise OperationalException(
|
Bittrex._validate_response(data)
|
||||||
'{message}'.format(
|
raise OperationalException('{message}'.format(message=data['message']))
|
||||||
message=data['message']))
|
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
||||||
|
|
||||||
def get_market_summaries(self) -> List[Dict]:
|
def get_market_summaries(self) -> List[Dict]:
|
||||||
data = _API.get_market_summaries()
|
data = _API.get_market_summaries()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise OperationalException(
|
Bittrex._validate_response(data)
|
||||||
'{message}'.format(
|
raise OperationalException('{message}'.format(message=data['message']))
|
||||||
message=data['message']))
|
|
||||||
return data['result']
|
return data['result']
|
||||||
|
|
||||||
def get_wallet_health(self) -> List[Dict]:
|
def get_wallet_health(self) -> List[Dict]:
|
||||||
data = _API_V2.get_wallet_health()
|
data = _API_V2.get_wallet_health()
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise OperationalException(
|
Bittrex._validate_response(data)
|
||||||
'{message}'.format(
|
raise OperationalException('{message}'.format(message=data['message']))
|
||||||
message=data['message']))
|
|
||||||
return [{
|
return [{
|
||||||
'Currency': entry['Health']['Currency'],
|
'Currency': entry['Health']['Currency'],
|
||||||
'IsActive': entry['Health']['IsActive'],
|
'IsActive': entry['Health']['IsActive'],
|
||||||
|
156
freqtrade/fiat_convert.py
Normal file
156
freqtrade/fiat_convert.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from pymarketcap import Pymarketcap
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoFiat():
|
||||||
|
# Constants
|
||||||
|
CACHE_DURATION = 6 * 60 * 60 # 6 hours
|
||||||
|
|
||||||
|
def __init__(self, crypto_symbol: str, fiat_symbol: str, price: float) -> None:
|
||||||
|
"""
|
||||||
|
Create an object that will contains the price for a crypto-currency in fiat
|
||||||
|
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
|
||||||
|
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
|
||||||
|
:param price: Price in FIAT
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Public attributes
|
||||||
|
self.crypto_symbol = None
|
||||||
|
self.fiat_symbol = None
|
||||||
|
self.price = 0.0
|
||||||
|
|
||||||
|
# Private attributes
|
||||||
|
self._expiration = 0
|
||||||
|
|
||||||
|
self.crypto_symbol = crypto_symbol.upper()
|
||||||
|
self.fiat_symbol = fiat_symbol.upper()
|
||||||
|
self.set_price(price=price)
|
||||||
|
|
||||||
|
def set_price(self, price: float) -> None:
|
||||||
|
"""
|
||||||
|
Set the price of the Crypto-currency in FIAT and set the expiration time
|
||||||
|
:param price: Price of the current Crypto currency in the fiat
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.price = price
|
||||||
|
self._expiration = time.time() + self.CACHE_DURATION
|
||||||
|
|
||||||
|
def is_expired(self) -> bool:
|
||||||
|
"""
|
||||||
|
Return if the current price is still valid or needs to be refreshed
|
||||||
|
:return: bool, true the price is expired and needs to be refreshed, false the price is
|
||||||
|
still valid
|
||||||
|
"""
|
||||||
|
return self._expiration - time.time() <= 0
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoToFiatConverter():
|
||||||
|
# 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"
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._coinmarketcap = Pymarketcap()
|
||||||
|
self._pairs = []
|
||||||
|
|
||||||
|
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
|
"""
|
||||||
|
Convert an amount of crypto-currency to fiat
|
||||||
|
:param crypto_amount: amount of crypto-currency to convert
|
||||||
|
:param crypto_symbol: crypto-currency used
|
||||||
|
:param fiat_symbol: fiat to convert to
|
||||||
|
:return: float, value in fiat of the crypto-currency amount
|
||||||
|
"""
|
||||||
|
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
|
||||||
|
return float(crypto_amount) * float(price)
|
||||||
|
|
||||||
|
def get_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
|
"""
|
||||||
|
Return the price of the Crypto-currency in Fiat
|
||||||
|
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
|
||||||
|
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
|
||||||
|
:return: Price in FIAT
|
||||||
|
"""
|
||||||
|
crypto_symbol = crypto_symbol.upper()
|
||||||
|
fiat_symbol = fiat_symbol.upper()
|
||||||
|
|
||||||
|
# Check if the fiat convertion you want is supported
|
||||||
|
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||||
|
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
||||||
|
|
||||||
|
# Get the pair that interest us and return the price in fiat
|
||||||
|
for pair in self._pairs:
|
||||||
|
if pair.crypto_symbol == crypto_symbol and pair.fiat_symbol == fiat_symbol:
|
||||||
|
# If the price is expired we refresh it, avoid to call the API all the time
|
||||||
|
if pair.is_expired():
|
||||||
|
pair.set_price(
|
||||||
|
price=self._find_price(
|
||||||
|
crypto_symbol=pair.crypto_symbol,
|
||||||
|
fiat_symbol=pair.fiat_symbol
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# return the last price we have for this pair
|
||||||
|
return pair.price
|
||||||
|
|
||||||
|
# The pair does not exist, so we create it and return the price
|
||||||
|
return self._add_pair(
|
||||||
|
crypto_symbol=crypto_symbol,
|
||||||
|
fiat_symbol=fiat_symbol,
|
||||||
|
price=self._find_price(
|
||||||
|
crypto_symbol=crypto_symbol,
|
||||||
|
fiat_symbol=fiat_symbol
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _add_pair(self, crypto_symbol: str, fiat_symbol: str, price: float) -> float:
|
||||||
|
"""
|
||||||
|
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
|
||||||
|
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
|
||||||
|
:return: price in FIAT
|
||||||
|
"""
|
||||||
|
self._pairs.append(
|
||||||
|
CryptoFiat(
|
||||||
|
crypto_symbol=crypto_symbol,
|
||||||
|
fiat_symbol=fiat_symbol,
|
||||||
|
price=price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return price
|
||||||
|
|
||||||
|
def _is_supported_fiat(self, fiat: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the FIAT your want to convert to is supported
|
||||||
|
:param fiat: FIAT to check (e.g USD)
|
||||||
|
:return: bool, True supported, False not supported
|
||||||
|
"""
|
||||||
|
|
||||||
|
fiat = fiat.upper()
|
||||||
|
|
||||||
|
return fiat in self.SUPPORTED_FIAT
|
||||||
|
|
||||||
|
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||||
|
"""
|
||||||
|
Call CoinMarketCap API to retrieve the price in the FIAT
|
||||||
|
:param crypto_symbol: Crypto-currency you want to convert (e.g BTC)
|
||||||
|
:param fiat_symbol: FIAT currency you want to convert to (e.g USD)
|
||||||
|
:return: float, price of the crypto-currency in Fiat
|
||||||
|
"""
|
||||||
|
# Check if the fiat convertion you want is supported
|
||||||
|
if not self._is_supported_fiat(fiat=fiat_symbol):
|
||||||
|
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
|
||||||
|
|
||||||
|
return float(
|
||||||
|
self._coinmarketcap.ticker(
|
||||||
|
currency=crypto_symbol,
|
||||||
|
convert=fiat_symbol
|
||||||
|
)['price_' + fiat_symbol.lower()]
|
||||||
|
)
|
@ -1,10 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional, List
|
from typing import Dict, Optional, List
|
||||||
|
|
||||||
@ -17,25 +19,24 @@ from freqtrade.analyze import get_signal, SignalType
|
|||||||
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
||||||
load_config
|
load_config
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None:
|
def refresh_whitelist(whitelist: List[str]) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Check wallet health and remove pair from whitelist if necessary
|
Check wallet health and remove pair from whitelist if necessary
|
||||||
:param whitelist: a new whitelist (optional)
|
:param whitelist: the pair the user might want to trade
|
||||||
:return: None
|
:return: the list of pairs the user wants to trade without the one unavailable or black_listed
|
||||||
"""
|
"""
|
||||||
whitelist = whitelist or _CONF['exchange']['pair_whitelist']
|
|
||||||
|
|
||||||
sanitized_whitelist = []
|
sanitized_whitelist = []
|
||||||
health = exchange.get_wallet_health()
|
health = exchange.get_wallet_health()
|
||||||
for status in health:
|
for status in health:
|
||||||
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
|
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
|
||||||
if pair not in whitelist:
|
if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []):
|
||||||
continue
|
continue
|
||||||
if status['IsActive']:
|
if status['IsActive']:
|
||||||
sanitized_whitelist.append(pair)
|
sanitized_whitelist.append(pair)
|
||||||
@ -44,24 +45,29 @@ def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None:
|
|||||||
'Ignoring %s from whitelist (reason: %s).',
|
'Ignoring %s from whitelist (reason: %s).',
|
||||||
pair, status.get('Notice') or 'wallet is not active'
|
pair, status.get('Notice') or 'wallet is not active'
|
||||||
)
|
)
|
||||||
if _CONF['exchange']['pair_whitelist'] != sanitized_whitelist:
|
return sanitized_whitelist
|
||||||
logger.debug('Using refreshed pair whitelist: %s ...', sanitized_whitelist)
|
|
||||||
_CONF['exchange']['pair_whitelist'] = sanitized_whitelist
|
|
||||||
|
|
||||||
|
|
||||||
def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
def _process(nb_assets: Optional[int] = 0) -> bool:
|
||||||
"""
|
"""
|
||||||
Queries the persistence layer for open trades and handles them,
|
Queries the persistence layer for open trades and handles them,
|
||||||
otherwise a new trade is created.
|
otherwise a new trade is created.
|
||||||
:param: dynamic_whitelist: True is a dynamic whitelist should be generated (optional)
|
:param: nb_assets: the maximum number of pairs to be traded at the same time
|
||||||
:return: True if a trade has been created or closed, False otherwise
|
:return: True if a trade has been created or closed, False otherwise
|
||||||
"""
|
"""
|
||||||
state_changed = False
|
state_changed = False
|
||||||
try:
|
try:
|
||||||
# Refresh whitelist based on wallet maintenance
|
# Refresh whitelist based on wallet maintenance
|
||||||
refresh_whitelist(
|
sanitized_list = refresh_whitelist(
|
||||||
gen_pair_whitelist(_CONF['stake_currency']) if dynamic_whitelist else None
|
gen_pair_whitelist(
|
||||||
|
_CONF['stake_currency']
|
||||||
|
) if nb_assets else _CONF['exchange']['pair_whitelist']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Keep only the subsets of pairs wanted (up to nb_assets)
|
||||||
|
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
||||||
|
_CONF['exchange']['pair_whitelist'] = final_list
|
||||||
|
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
if len(trades) < _CONF['max_open_trades']:
|
if len(trades) < _CONF['max_open_trades']:
|
||||||
@ -73,8 +79,8 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
|||||||
'Checked all whitelisted currencies. '
|
'Checked all whitelisted currencies. '
|
||||||
'Found no suitable entry positions for buying. Will keep looking ...'
|
'Found no suitable entry positions for buying. Will keep looking ...'
|
||||||
)
|
)
|
||||||
except DependencyException as e:
|
except DependencyException as exception:
|
||||||
logger.warning('Unable to create trade: %s', e)
|
logger.warning('Unable to create trade: %s', exception)
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# Get order details for actual price per unit
|
# Get order details for actual price per unit
|
||||||
@ -115,14 +121,42 @@ def execute_sell(trade: Trade, limit: float) -> None:
|
|||||||
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
|
||||||
rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
profit_trade = trade.calc_profit(rate=limit)
|
||||||
trade.exchange,
|
|
||||||
trade.pair.replace('_', '/'),
|
message = '*{exchange}:* Selling [{pair}]({pair_url}) with limit `{limit:.8f}`'.format(
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange=trade.exchange,
|
||||||
limit,
|
pair=trade.pair.replace('_', '/'),
|
||||||
fmt_exp_profit
|
pair_url=exchange.get_pair_detail_url(trade.pair),
|
||||||
))
|
limit=limit
|
||||||
|
)
|
||||||
|
|
||||||
|
# For regular case, when the configuration exists
|
||||||
|
if 'stake_currency' in _CONF and 'fiat_display_currency' in _CONF:
|
||||||
|
fiat_converter = CryptoToFiatConverter()
|
||||||
|
profit_fiat = fiat_converter.convert_amount(
|
||||||
|
profit_trade,
|
||||||
|
_CONF['stake_currency'],
|
||||||
|
_CONF['fiat_display_currency']
|
||||||
|
)
|
||||||
|
message += '` (profit: ~{profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
|
||||||
|
'` / {profit_fiat:.3f} {fiat})`'.format(
|
||||||
|
profit_percent=fmt_exp_profit,
|
||||||
|
profit_coin=profit_trade,
|
||||||
|
coin=_CONF['stake_currency'],
|
||||||
|
profit_fiat=profit_fiat,
|
||||||
|
fiat=_CONF['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 += '` (profit: ~{profit_percent:.2f}%, {profit_coin:.8f})`'.format(
|
||||||
|
profit_percent=fmt_exp_profit,
|
||||||
|
profit_coin=profit_trade
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send the message
|
||||||
|
rpc.send_msg(message)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
|
|
||||||
@ -131,7 +165,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
|
|||||||
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
current_profit = trade.calc_profit(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
return True
|
return True
|
||||||
@ -142,7 +176,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
|
|||||||
if time_diff > float(duration) and current_profit > threshold:
|
if time_diff > float(duration) and current_profit > threshold:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
|
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@ -158,18 +192,21 @@ def handle_trade(trade: Trade) -> bool:
|
|||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
|
||||||
# Check if minimal roi has been reached
|
# Check if minimal roi has been reached
|
||||||
if not min_roi_reached(trade, current_rate, datetime.utcnow()):
|
if min_roi_reached(trade, current_rate, datetime.utcnow()):
|
||||||
return False
|
logger.debug('Executing sell due to ROI ...')
|
||||||
|
execute_sell(trade, current_rate)
|
||||||
|
return True
|
||||||
|
|
||||||
# Check if sell signal has been enabled and triggered
|
# Check if sell signal has been enabled and triggered
|
||||||
if _CONF.get('experimental', {}).get('use_sell_signal'):
|
if _CONF.get('experimental', {}).get('use_sell_signal'):
|
||||||
logger.debug('Checking sell_signal ...')
|
logger.debug('Checking sell_signal ...')
|
||||||
if not get_signal(trade.pair, SignalType.SELL):
|
if get_signal(trade.pair, SignalType.SELL):
|
||||||
return False
|
logger.debug('Executing sell due to sell signal ...')
|
||||||
|
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_target_bid(ticker: Dict[str, float]) -> float:
|
def get_target_bid(ticker: Dict[str, float]) -> float:
|
||||||
""" Calculates bid target between current ask price and last price """
|
""" Calculates bid target between current ask price and last price """
|
||||||
@ -206,8 +243,17 @@ def create_trade(stake_amount: float) -> bool:
|
|||||||
raise DependencyException('No pair in whitelist')
|
raise DependencyException('No pair in whitelist')
|
||||||
|
|
||||||
# Pick pair based on StochRSI buy signals
|
# Pick pair based on StochRSI buy signals
|
||||||
for _pair in whitelist:
|
with ThreadPoolExecutor() as pool:
|
||||||
if get_signal(_pair, SignalType.BUY):
|
awaitable_signals = [
|
||||||
|
asyncio.wrap_future(pool.submit(get_signal, pair, SignalType.BUY))
|
||||||
|
for pair in whitelist
|
||||||
|
]
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
signals = loop.run_until_complete(asyncio.gather(*awaitable_signals))
|
||||||
|
|
||||||
|
for idx, _pair in enumerate(whitelist):
|
||||||
|
if signals[idx]:
|
||||||
pair = _pair
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -230,7 +276,7 @@ def create_trade(stake_amount: float) -> bool:
|
|||||||
pair=pair,
|
pair=pair,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
fee=exchange.get_fee() * 2,
|
fee=exchange.get_fee(),
|
||||||
open_rate=buy_limit,
|
open_rate=buy_limit,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
exchange=exchange.get_name().upper(),
|
exchange=exchange.get_name().upper(),
|
||||||
@ -262,11 +308,10 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@cached(TTLCache(maxsize=1, ttl=1800))
|
@cached(TTLCache(maxsize=1, ttl=1800))
|
||||||
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
|
def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]:
|
||||||
"""
|
"""
|
||||||
Updates the whitelist with with a dynamically generated list
|
Updates the whitelist with with a dynamically generated list
|
||||||
:param base_currency: base currency as str
|
:param base_currency: base currency as str
|
||||||
:param topn: maximum number of returned results
|
|
||||||
:param key: sort key (defaults to 'BaseVolume')
|
:param key: sort key (defaults to 'BaseVolume')
|
||||||
:return: List of pairs
|
:return: List of pairs
|
||||||
"""
|
"""
|
||||||
@ -275,7 +320,8 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum
|
|||||||
key=lambda s: s.get(key) or 0.0,
|
key=lambda s: s.get(key) or 0.0,
|
||||||
reverse=True
|
reverse=True
|
||||||
)
|
)
|
||||||
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
|
|
||||||
|
return [s['MarketName'].replace('-', '_') for s in summaries]
|
||||||
|
|
||||||
|
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
@ -320,6 +366,16 @@ def main() -> None:
|
|||||||
if args.dynamic_whitelist:
|
if args.dynamic_whitelist:
|
||||||
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
|
||||||
|
|
||||||
|
# If the user ask for Dry run with a local DB instead of memory
|
||||||
|
if args.dry_run_db:
|
||||||
|
if _CONF.get('dry_run', False):
|
||||||
|
_CONF.update({'dry_run_db': True})
|
||||||
|
logger.info(
|
||||||
|
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite". (--dry_run_db detected)'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
init(_CONF)
|
init(_CONF)
|
||||||
old_state = None
|
old_state = None
|
||||||
@ -336,7 +392,7 @@ def main() -> None:
|
|||||||
throttle(
|
throttle(
|
||||||
_process,
|
_process,
|
||||||
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
|
||||||
dynamic_whitelist=args.dynamic_whitelist,
|
nb_assets=args.dynamic_whitelist,
|
||||||
)
|
)
|
||||||
old_state = new_state
|
old_state = new_state
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
@ -112,8 +112,19 @@ def parse_args(args: List[str]):
|
|||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--dynamic-whitelist',
|
'--dynamic-whitelist',
|
||||||
help='dynamically generate and update whitelist based on 24h BaseVolume',
|
help='dynamically generate and update whitelist based on 24h BaseVolume (Default 20 currencies)', # noqa
|
||||||
|
dest='dynamic_whitelist',
|
||||||
|
const=20,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
nargs='?',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run-db',
|
||||||
|
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" instead of memory DB. Work only if dry_run is \
|
||||||
|
enabled.', # noqa
|
||||||
action='store_true',
|
action='store_true',
|
||||||
|
dest='dry_run_db',
|
||||||
)
|
)
|
||||||
build_subcommands(parser)
|
build_subcommands(parser)
|
||||||
parsed_args = parser.parse_args(args)
|
parsed_args = parser.parse_args(args)
|
||||||
@ -155,6 +166,13 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
dest='realistic_simulation',
|
dest='realistic_simulation',
|
||||||
)
|
)
|
||||||
|
backtesting_cmd.add_argument(
|
||||||
|
'-r', '--refresh-pairs-cached',
|
||||||
|
help='refresh the pairs files in tests/testdata with the latest data from Bittrex. \
|
||||||
|
Use it if you want to run your backtesting with up-to-date data.',
|
||||||
|
action='store_true',
|
||||||
|
dest='refresh_pairs',
|
||||||
|
)
|
||||||
|
|
||||||
# Add hyperopt subcommand
|
# Add hyperopt subcommand
|
||||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
||||||
@ -173,6 +191,14 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
|||||||
dest='mongodb',
|
dest='mongodb',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
)
|
)
|
||||||
|
hyperopt_cmd.add_argument(
|
||||||
|
'-i', '--ticker-interval',
|
||||||
|
help='specify ticker interval in minutes (default: 5)',
|
||||||
|
dest='ticker_interval',
|
||||||
|
default=5,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
@ -182,6 +208,7 @@ CONF_SCHEMA = {
|
|||||||
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
'max_open_trades': {'type': 'integer', 'minimum': 1},
|
||||||
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
|
||||||
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
'stake_amount': {'type': 'number', 'minimum': 0.0005},
|
||||||
|
'fiat_display_currency': {'type': 'string', 'enum': ['USD', 'EUR', 'CAD', 'SGD']},
|
||||||
'dry_run': {'type': 'boolean'},
|
'dry_run': {'type': 'boolean'},
|
||||||
'minimal_roi': {
|
'minimal_roi': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -241,6 +268,14 @@ CONF_SCHEMA = {
|
|||||||
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
},
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
|
},
|
||||||
|
'pair_blacklist': {
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
|
'uniqueItems': True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||||
@ -253,6 +288,7 @@ CONF_SCHEMA = {
|
|||||||
'max_open_trades',
|
'max_open_trades',
|
||||||
'stake_currency',
|
'stake_currency',
|
||||||
'stake_amount',
|
'stake_amount',
|
||||||
|
'fiat_display_currency',
|
||||||
'dry_run',
|
'dry_run',
|
||||||
'minimal_roi',
|
'minimal_roi',
|
||||||
'bid_strategy',
|
'bid_strategy',
|
||||||
|
@ -1,41 +1,117 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
|
||||||
|
import logging
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from freqtrade.exchange import get_ticker_history
|
||||||
|
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
||||||
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
|
from freqtrade.analyze import populate_indicators, parse_ticker_dataframe
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def load_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None) -> Dict[str, List]:
|
|
||||||
|
def load_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None,
|
||||||
|
refresh_pairs: Optional[bool] = False) -> Dict[str, List]:
|
||||||
"""
|
"""
|
||||||
Loads ticker history data for the given parameters
|
Loads ticker history data for the given parameters
|
||||||
:param ticker_interval: ticker interval in minutes
|
:param ticker_interval: ticker interval in minutes
|
||||||
:param pairs: list of pairs
|
:param pairs: list of pairs
|
||||||
:return: dict
|
:return: dict
|
||||||
"""
|
"""
|
||||||
path = os.path.abspath(os.path.dirname(__file__))
|
path = testdata_path()
|
||||||
result = {}
|
result = {}
|
||||||
_pairs = pairs or [
|
|
||||||
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
|
_pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist']
|
||||||
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
|
|
||||||
]
|
# If the user force the refresh of pairs
|
||||||
|
if refresh_pairs:
|
||||||
|
logger.info('Download data for all pairs and store them in freqtrade/tests/testsdata')
|
||||||
|
download_pairs(_pairs)
|
||||||
|
|
||||||
for pair in _pairs:
|
for pair in _pairs:
|
||||||
with open('{abspath}/../tests/testdata/{pair}-{ticker_interval}.json'.format(
|
file = '{abspath}/{pair}-{ticker_interval}.json'.format(
|
||||||
abspath=path,
|
abspath=path,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
ticker_interval=ticker_interval,
|
ticker_interval=ticker_interval,
|
||||||
)) as tickerdata:
|
)
|
||||||
|
# The file does not exist we download it
|
||||||
|
if not os.path.isfile(file):
|
||||||
|
download_backtesting_testdata(pair=pair, interval=ticker_interval)
|
||||||
|
|
||||||
|
# Read the file, load the json
|
||||||
|
with open(file) as tickerdata:
|
||||||
result[pair] = json.load(tickerdata)
|
result[pair] = json.load(tickerdata)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
def preprocess(tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
|
||||||
"""Creates a dataframe and populates indicators for given ticker data"""
|
"""Creates a dataframe and populates indicators for given ticker data"""
|
||||||
processed = {}
|
return {pair: populate_indicators(parse_ticker_dataframe(pair_data))
|
||||||
for pair, pair_data in tickerdata.items():
|
for pair, pair_data in tickerdata.items()}
|
||||||
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
|
|
||||||
return processed
|
|
||||||
|
def testdata_path() -> str:
|
||||||
|
"""Return the path where testdata files are stored"""
|
||||||
|
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'tests', 'testdata'))
|
||||||
|
|
||||||
|
|
||||||
|
def download_pairs(pairs: List[str]) -> bool:
|
||||||
|
"""For each pairs passed in parameters, download 1 and 5 ticker intervals"""
|
||||||
|
for pair in pairs:
|
||||||
|
try:
|
||||||
|
for interval in [1, 5]:
|
||||||
|
download_backtesting_testdata(pair=pair, interval=interval)
|
||||||
|
except BaseException:
|
||||||
|
logger.info('Failed to download the pair: "{pair}", Interval: {interval} min'.format(
|
||||||
|
pair=pair,
|
||||||
|
interval=interval,
|
||||||
|
))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def download_backtesting_testdata(pair: str, interval: int = 5) -> bool:
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
:param pairs: list of pairs to download
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
path = testdata_path()
|
||||||
|
logger.info('Download the pair: "{pair}", Interval: {interval} min'.format(
|
||||||
|
pair=pair,
|
||||||
|
interval=interval,
|
||||||
|
))
|
||||||
|
|
||||||
|
filepair = pair.replace("-", "_")
|
||||||
|
filename = os.path.join(path, '{pair}-{interval}.json'.format(
|
||||||
|
pair=filepair,
|
||||||
|
interval=interval,
|
||||||
|
))
|
||||||
|
filename = filename.replace('USDT_BTC', 'BTC_FAKEBULL')
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
with open(filename, "rt") as fp:
|
||||||
|
data = json.load(fp)
|
||||||
|
logger.debug("Current Start: {}".format(data[1]['T']))
|
||||||
|
logger.debug("Current End: {}".format(data[-1:][0]['T']))
|
||||||
|
else:
|
||||||
|
data = []
|
||||||
|
logger.debug("Current Start: None")
|
||||||
|
logger.debug("Current End: None")
|
||||||
|
|
||||||
|
new_data = get_ticker_history(pair=pair, tick_interval=int(interval))
|
||||||
|
for row in new_data:
|
||||||
|
if row not in data:
|
||||||
|
data.append(row)
|
||||||
|
logger.debug("New Start: {}".format(data[1]['T']))
|
||||||
|
logger.debug("New End: {}".format(data[-1:][0]['T']))
|
||||||
|
data = sorted(data, key=lambda data: data['T'])
|
||||||
|
|
||||||
|
with open(filename, "wt") as fp:
|
||||||
|
json.dump(data, fp)
|
||||||
|
|
||||||
|
return True
|
||||||
|
@ -35,7 +35,8 @@ def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
|||||||
return arrow.get(min_date), arrow.get(max_date)
|
return arrow.get(min_date), arrow.get(max_date)
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currency) -> str:
|
def generate_text_table(
|
||||||
|
data: Dict[str, Dict], results: DataFrame, stake_currency, ticker_interval) -> str:
|
||||||
"""
|
"""
|
||||||
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
|
||||||
@ -47,34 +48,34 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc
|
|||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
pair,
|
pair,
|
||||||
len(result.index),
|
len(result.index),
|
||||||
'{:.2f}%'.format(result.profit.mean() * 100.0),
|
'{:.2f}%'.format(result.profit_percent.mean() * 100.0),
|
||||||
'{:.08f} {}'.format(result.profit.sum(), stake_currency),
|
'{:.08f} {}'.format(result.profit_BTC.sum(), stake_currency),
|
||||||
'{:.2f}'.format(result.duration.mean() * 5),
|
'{:.2f}'.format(result.duration.mean() * ticker_interval),
|
||||||
])
|
])
|
||||||
|
|
||||||
# Append Total
|
# Append Total
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
'TOTAL',
|
'TOTAL',
|
||||||
len(results.index),
|
len(results.index),
|
||||||
'{:.2f}%'.format(results.profit.mean() * 100.0),
|
'{:.2f}%'.format(results.profit_percent.mean() * 100.0),
|
||||||
'{:.08f} {}'.format(results.profit.sum(), stake_currency),
|
'{:.08f} {}'.format(results.profit_BTC.sum(), stake_currency),
|
||||||
'{:.2f}'.format(results.duration.mean() * 5),
|
'{:.2f}'.format(results.duration.mean() * ticker_interval),
|
||||||
])
|
])
|
||||||
return tabulate(tabular_data, headers=headers)
|
return tabulate(tabular_data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
def backtest(config: Dict, processed: Dict[str, DataFrame],
|
def backtest(stake_amount: float, processed: Dict[str, DataFrame],
|
||||||
max_open_trades: int = 0, realistic: bool = True) -> DataFrame:
|
max_open_trades: int = 0, realistic: bool = True) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Implements backtesting functionality
|
Implements backtesting functionality
|
||||||
:param config: config to use
|
:param stake_amount: btc amount to use for each trade
|
||||||
:param processed: a processed dictionary with format {pair, data}
|
:param processed: a processed dictionary with format {pair, data}
|
||||||
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
||||||
:param realistic: do we try to simulate realistic trades? (default: True)
|
:param realistic: do we try to simulate realistic trades? (default: True)
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
trades = []
|
trades = []
|
||||||
trade_count_lock = {}
|
trade_count_lock: dict = {}
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
for pair, pair_data in processed.items():
|
for pair, pair_data in processed.items():
|
||||||
pair_data['buy'], pair_data['sell'] = 0, 0
|
pair_data['buy'], pair_data['sell'] = 0, 0
|
||||||
@ -97,8 +98,9 @@ def backtest(config: Dict, processed: Dict[str, DataFrame],
|
|||||||
trade = Trade(
|
trade = Trade(
|
||||||
open_rate=row.close,
|
open_rate=row.close,
|
||||||
open_date=row.date,
|
open_date=row.date,
|
||||||
amount=config['stake_amount'],
|
stake_amount=stake_amount,
|
||||||
fee=exchange.get_fee() * 2
|
amount=stake_amount / row.open,
|
||||||
|
fee=exchange.get_fee()
|
||||||
)
|
)
|
||||||
|
|
||||||
# calculate win/lose forwards from buy point
|
# calculate win/lose forwards from buy point
|
||||||
@ -108,12 +110,20 @@ def backtest(config: Dict, processed: Dict[str, DataFrame],
|
|||||||
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
||||||
|
|
||||||
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
|
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
|
||||||
current_profit = trade.calc_profit(row2.close)
|
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
|
||||||
|
current_profit_btc = trade.calc_profit(rate=row2.close)
|
||||||
lock_pair_until = row2.Index
|
lock_pair_until = row2.Index
|
||||||
|
|
||||||
trades.append((pair, current_profit, row2.Index - row.Index))
|
trades.append(
|
||||||
|
(
|
||||||
|
pair,
|
||||||
|
current_profit_percent,
|
||||||
|
current_profit_btc,
|
||||||
|
row2.Index - row.Index
|
||||||
|
)
|
||||||
|
)
|
||||||
break
|
break
|
||||||
labels = ['currency', 'profit', 'duration']
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
||||||
return DataFrame.from_records(trades, columns=labels)
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
|
||||||
@ -132,13 +142,15 @@ def start(args):
|
|||||||
logger.info('Using ticker_interval: %s ...', args.ticker_interval)
|
logger.info('Using ticker_interval: %s ...', args.ticker_interval)
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
|
pairs = config['exchange']['pair_whitelist']
|
||||||
if args.live:
|
if args.live:
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
for pair in config['exchange']['pair_whitelist']:
|
for pair in pairs:
|
||||||
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
|
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
|
||||||
else:
|
else:
|
||||||
logger.info('Using local backtesting data (ignoring whitelist in given config) ...')
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
data = load_data(args.ticker_interval)
|
data = load_data(pairs=pairs, ticker_interval=args.ticker_interval,
|
||||||
|
refresh_pairs=args.refresh_pairs)
|
||||||
|
|
||||||
logger.info('Using stake_currency: %s ...', config['stake_currency'])
|
logger.info('Using stake_currency: %s ...', config['stake_currency'])
|
||||||
logger.info('Using stake_amount: %s ...', config['stake_amount'])
|
logger.info('Using stake_amount: %s ...', config['stake_amount'])
|
||||||
@ -158,9 +170,9 @@ def start(args):
|
|||||||
|
|
||||||
# Execute backtest and print results
|
# Execute backtest and print results
|
||||||
results = backtest(
|
results = backtest(
|
||||||
config, preprocess(data), max_open_trades, args.realistic_simulation
|
config['stake_amount'], preprocess(data), max_open_trades, args.realistic_simulation
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
'\n====================== BACKTESTING REPORT ======================================\n%s',
|
'\n====================== BACKTESTING REPORT ======================================\n%s',
|
||||||
generate_text_table(data, results, config['stake_currency'])
|
generate_text_table(data, results, config['stake_currency'], args.ticker_interval)
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
# pragma pylint: disable=missing-docstring,W0212,W0603
|
||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -8,13 +8,15 @@ from functools import reduce
|
|||||||
from math import exp
|
from math import exp
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
|
||||||
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK, STATUS_FAIL
|
||||||
from hyperopt.mongoexp import MongoTrials
|
from hyperopt.mongoexp import MongoTrials
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import exchange, optimize
|
from freqtrade import exchange, optimize
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
|
from freqtrade.misc import load_config
|
||||||
from freqtrade.optimize.backtesting import backtest
|
from freqtrade.optimize.backtesting import backtest
|
||||||
|
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
||||||
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||||
|
|
||||||
# Remove noisy log messages
|
# Remove noisy log messages
|
||||||
@ -23,33 +25,22 @@ logging.getLogger('hyperopt.tpe').setLevel(logging.WARNING)
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
||||||
TARGET_TRADES = 1100
|
TARGET_TRADES = 1100
|
||||||
TOTAL_TRIES = None
|
TOTAL_TRIES = None
|
||||||
_CURRENT_TRIES = 0
|
_CURRENT_TRIES = 0
|
||||||
|
CURRENT_BEST_LOSS = 100
|
||||||
|
|
||||||
TOTAL_PROFIT_TO_BEAT = 3
|
# this is expexted avg profit * expected trade count
|
||||||
AVG_PROFIT_TO_BEAT = 0.2
|
# for example 3.5%, 1100 trades, EXPECTED_MAX_PROFIT = 3.85
|
||||||
AVG_DURATION_TO_BEAT = 50
|
EXPECTED_MAX_PROFIT = 3.85
|
||||||
|
|
||||||
# Configuration and data used by hyperopt
|
# Configuration and data used by hyperopt
|
||||||
PROCESSED = optimize.preprocess(optimize.load_data())
|
PROCESSED = optimize.preprocess(optimize.load_data())
|
||||||
OPTIMIZE_CONFIG = {
|
OPTIMIZE_CONFIG = hyperopt_optimize_conf()
|
||||||
'max_open_trades': 3,
|
|
||||||
'stake_currency': 'BTC',
|
|
||||||
'stake_amount': 0.01,
|
|
||||||
'minimal_roi': {
|
|
||||||
'40': 0.0,
|
|
||||||
'30': 0.01,
|
|
||||||
'20': 0.02,
|
|
||||||
'0': 0.04,
|
|
||||||
},
|
|
||||||
'stoploss': -0.10,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Monkey patch config
|
# Monkey patch config
|
||||||
from freqtrade import main
|
from freqtrade import main # noqa
|
||||||
main._CONF = OPTIMIZE_CONFIG
|
main._CONF = OPTIMIZE_CONFIG
|
||||||
|
|
||||||
|
|
||||||
@ -102,69 +93,72 @@ SPACE = {
|
|||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def log_results(results):
|
def log_results(results):
|
||||||
"if results is better than _TO_BEAT show it"
|
""" log results if it is better than any previous evaluation """
|
||||||
|
global CURRENT_BEST_LOSS
|
||||||
|
|
||||||
current_try = results['current_tries']
|
if results['loss'] < CURRENT_BEST_LOSS:
|
||||||
total_tries = results['total_tries']
|
CURRENT_BEST_LOSS = results['loss']
|
||||||
result = results['result']
|
logger.info('{:5d}/{}: {}'.format(
|
||||||
profit = results['total_profit'] / 1000
|
results['current_tries'],
|
||||||
|
results['total_tries'],
|
||||||
outcome = '{:5d}/{}: {}'.format(current_try, total_tries, result)
|
results['result']))
|
||||||
|
|
||||||
if profit >= TOTAL_PROFIT_TO_BEAT:
|
|
||||||
logger.info(outcome)
|
|
||||||
else:
|
else:
|
||||||
print('.', end='')
|
print('.', end='')
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_loss(total_profit: float, trade_count: int):
|
||||||
|
""" objective function, returns smaller number for more optimal results """
|
||||||
|
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
||||||
|
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
|
||||||
|
return trade_loss + profit_loss
|
||||||
|
|
||||||
|
|
||||||
def optimizer(params):
|
def optimizer(params):
|
||||||
global _CURRENT_TRIES
|
global _CURRENT_TRIES
|
||||||
|
|
||||||
from freqtrade.optimize import backtesting
|
from freqtrade.optimize import backtesting
|
||||||
backtesting.populate_buy_trend = buy_strategy_generator(params)
|
backtesting.populate_buy_trend = buy_strategy_generator(params)
|
||||||
|
|
||||||
results = backtest(OPTIMIZE_CONFIG, PROCESSED)
|
results = backtest(OPTIMIZE_CONFIG['stake_amount'], PROCESSED)
|
||||||
|
result_explanation = format_results(results)
|
||||||
|
|
||||||
result = format_results(results)
|
total_profit = results.profit_percent.sum()
|
||||||
|
|
||||||
total_profit = results.profit.sum() * 1000
|
|
||||||
trade_count = len(results.index)
|
trade_count = len(results.index)
|
||||||
|
|
||||||
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
if trade_count == 0:
|
||||||
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
|
print('.', end='')
|
||||||
|
return {
|
||||||
|
'status': STATUS_FAIL,
|
||||||
|
'loss': float('inf')
|
||||||
|
}
|
||||||
|
|
||||||
|
loss = calculate_loss(total_profit, trade_count)
|
||||||
|
|
||||||
_CURRENT_TRIES += 1
|
_CURRENT_TRIES += 1
|
||||||
|
|
||||||
result_data = {
|
log_results({
|
||||||
'trade_count': trade_count,
|
'loss': loss,
|
||||||
'total_profit': total_profit,
|
|
||||||
'trade_loss': trade_loss,
|
|
||||||
'profit_loss': profit_loss,
|
|
||||||
'avg_profit': results.profit.mean() * 100.0,
|
|
||||||
'avg_duration': results.duration.mean() * 5,
|
|
||||||
'current_tries': _CURRENT_TRIES,
|
'current_tries': _CURRENT_TRIES,
|
||||||
'total_tries': TOTAL_TRIES,
|
'total_tries': TOTAL_TRIES,
|
||||||
'result': result,
|
'result': result_explanation,
|
||||||
'results': results
|
})
|
||||||
}
|
|
||||||
|
|
||||||
# logger.info('{:5d}/{}: {}'.format(_CURRENT_TRIES, TOTAL_TRIES, result))
|
|
||||||
log_results(result_data)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'loss': trade_loss + profit_loss,
|
'loss': loss,
|
||||||
'status': STATUS_OK,
|
'status': STATUS_OK,
|
||||||
'result': result
|
'result': result_explanation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def format_results(results: DataFrame):
|
def format_results(results: DataFrame):
|
||||||
return ('Made {:6d} buys. Average profit {: 5.2f}%. '
|
return ('{:6d} trades. Avg profit {: 5.2f}%. '
|
||||||
'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format(
|
'Total profit {: 11.8f} BTC. Avg duration {:5.1f} mins.').format(
|
||||||
len(results.index),
|
len(results.index),
|
||||||
results.profit.mean() * 100.0,
|
results.profit_percent.mean() * 100.0,
|
||||||
results.profit.sum(),
|
results.profit_BTC.sum(),
|
||||||
results.duration.mean() * 5,
|
results.duration.mean() * 5,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -215,7 +209,7 @@ def buy_strategy_generator(params):
|
|||||||
|
|
||||||
|
|
||||||
def start(args):
|
def start(args):
|
||||||
global TOTAL_TRIES
|
global TOTAL_TRIES, PROCESSED
|
||||||
TOTAL_TRIES = args.epochs
|
TOTAL_TRIES = args.epochs
|
||||||
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
@ -223,9 +217,15 @@ def start(args):
|
|||||||
# Initialize logger
|
# Initialize logger
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=args.loglevel,
|
level=args.loglevel,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
format='\n%(message)s',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger.info('Using config: %s ...', args.config)
|
||||||
|
config = load_config(args.config)
|
||||||
|
pairs = config['exchange']['pair_whitelist']
|
||||||
|
PROCESSED = optimize.preprocess(optimize.load_data(
|
||||||
|
pairs=pairs, ticker_interval=args.ticker_interval))
|
||||||
|
|
||||||
if args.mongodb:
|
if args.mongodb:
|
||||||
logger.info('Using mongodb ...')
|
logger.info('Using mongodb ...')
|
||||||
logger.info('Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!')
|
logger.info('Start scripts/start-mongodb.sh and start-hyperopt-worker.sh manually!')
|
||||||
@ -237,5 +237,6 @@ def start(args):
|
|||||||
|
|
||||||
best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
|
best = fmin(fn=optimizer, space=SPACE, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
|
||||||
logger.info('Best parameters:\n%s', json.dumps(best, indent=4))
|
logger.info('Best parameters:\n%s', json.dumps(best, indent=4))
|
||||||
|
|
||||||
results = sorted(trials.results, key=itemgetter('loss'))
|
results = sorted(trials.results, key=itemgetter('loss'))
|
||||||
logger.info('Best Result:\n%s', results[0]['result'])
|
logger.info('Best Result:\n%s', results[0]['result'])
|
||||||
|
41
freqtrade/optimize/hyperopt_conf.py
Normal file
41
freqtrade/optimize/hyperopt_conf.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""
|
||||||
|
File that contains the configuration for Hyperopt
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def hyperopt_optimize_conf() -> dict:
|
||||||
|
"""
|
||||||
|
This function is used to define which parameters Hyperopt must used.
|
||||||
|
The "pair_whitelist" is only used is your are using Hyperopt with MongoDB,
|
||||||
|
without MongoDB, Hyperopt will use the pair your have set in your config file.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'max_open_trades': 3,
|
||||||
|
'stake_currency': 'BTC',
|
||||||
|
'stake_amount': 0.01,
|
||||||
|
"minimal_roi": {
|
||||||
|
'40': 0.0,
|
||||||
|
'30': 0.01,
|
||||||
|
'20': 0.02,
|
||||||
|
'0': 0.04,
|
||||||
|
},
|
||||||
|
'stoploss': -0.10,
|
||||||
|
"bid_strategy": {
|
||||||
|
"ask_last_balance": 0.0
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"pair_whitelist": [
|
||||||
|
"BTC_ETH",
|
||||||
|
"BTC_LTC",
|
||||||
|
"BTC_ETC",
|
||||||
|
"BTC_DASH",
|
||||||
|
"BTC_ZEC",
|
||||||
|
"BTC_XLM",
|
||||||
|
"BTC_NXT",
|
||||||
|
"BTC_POWR",
|
||||||
|
"BTC_ADA",
|
||||||
|
"BTC_XMR"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,11 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
|
|||||||
_CONF.update(config)
|
_CONF.update(config)
|
||||||
if not engine:
|
if not engine:
|
||||||
if _CONF.get('dry_run', False):
|
if _CONF.get('dry_run', False):
|
||||||
|
# the user wants dry run to use a DB
|
||||||
|
if _CONF.get('dry_run_db', False):
|
||||||
|
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
|
||||||
|
# Otherwise dry run will store in memory
|
||||||
|
else:
|
||||||
engine = create_engine('sqlite://',
|
engine = create_engine('sqlite://',
|
||||||
connect_args={'check_same_thread': False},
|
connect_args={'check_same_thread': False},
|
||||||
poolclass=StaticPool,
|
poolclass=StaticPool,
|
||||||
@ -82,32 +87,99 @@ class Trade(_DECL_BASE):
|
|||||||
:param order: order retrieved by exchange.get_order()
|
:param order: order retrieved by exchange.get_order()
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if not order['closed']:
|
# Ignore open and cancelled orders
|
||||||
|
if not order['closed'] or order['rate'] is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info('Updating trade (id=%d) ...', self.id)
|
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_BUY':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = order['rate']
|
self.open_rate = Decimal(order['rate'])
|
||||||
self.amount = order['amount']
|
self.amount = Decimal(order['amount'])
|
||||||
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
||||||
|
self.open_order_id = None
|
||||||
elif order['type'] == 'LIMIT_SELL':
|
elif order['type'] == 'LIMIT_SELL':
|
||||||
# Set close rate and set actual profit
|
self.close(order['rate'])
|
||||||
self.close_rate = order['rate']
|
else:
|
||||||
self.close_profit = self.calc_profit()
|
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
def close(self, rate: float) -> None:
|
||||||
|
"""
|
||||||
|
Sets close_rate to the given rate, calculates total profit
|
||||||
|
and marks trade as closed
|
||||||
|
"""
|
||||||
|
self.close_rate = Decimal(rate)
|
||||||
|
self.close_profit = self.calc_profit_percent()
|
||||||
self.close_date = datetime.utcnow()
|
self.close_date = datetime.utcnow()
|
||||||
self.is_open = False
|
self.is_open = False
|
||||||
|
self.open_order_id = None
|
||||||
logger.info(
|
logger.info(
|
||||||
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
||||||
self
|
self
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
|
||||||
|
|
||||||
self.open_order_id = None
|
def calc_open_trade_price(
|
||||||
Trade.session.flush()
|
self,
|
||||||
|
fee: Optional[float] = None) -> float:
|
||||||
|
"""
|
||||||
|
Calculate the open_rate in BTC
|
||||||
|
:param fee: fee to use on the open rate (optional).
|
||||||
|
If rate is not set self.fee will be used
|
||||||
|
:return: Price in BTC of the open trade
|
||||||
|
"""
|
||||||
|
getcontext().prec = 8
|
||||||
|
|
||||||
def calc_profit(self, rate: Optional[float] = None) -> float:
|
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
|
||||||
|
fees = buy_trade * Decimal(fee or self.fee)
|
||||||
|
return float(buy_trade + fees)
|
||||||
|
|
||||||
|
def calc_close_trade_price(
|
||||||
|
self,
|
||||||
|
rate: Optional[float] = None,
|
||||||
|
fee: Optional[float] = None) -> float:
|
||||||
|
"""
|
||||||
|
Calculate the close_rate in BTC
|
||||||
|
:param fee: fee to use on the close rate (optional).
|
||||||
|
If rate is not set self.fee will be used
|
||||||
|
:param rate: rate to compare with (optional).
|
||||||
|
If rate is not set self.close_rate will be used
|
||||||
|
:return: Price in BTC of the open trade
|
||||||
|
"""
|
||||||
|
getcontext().prec = 8
|
||||||
|
|
||||||
|
if rate is None and not self.close_rate:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
|
||||||
|
fees = sell_trade * Decimal(fee or self.fee)
|
||||||
|
return float(sell_trade - fees)
|
||||||
|
|
||||||
|
def calc_profit(
|
||||||
|
self,
|
||||||
|
rate: Optional[float] = None,
|
||||||
|
fee: Optional[float] = None) -> float:
|
||||||
|
"""
|
||||||
|
Calculate the profit in BTC between Close and Open trade
|
||||||
|
:param fee: fee to use on the close rate (optional).
|
||||||
|
If rate is not set self.fee will be used
|
||||||
|
:param rate: close rate to compare with (optional).
|
||||||
|
If rate is not set self.close_rate will be used
|
||||||
|
:return: profit in BTC as float
|
||||||
|
"""
|
||||||
|
open_trade_price = self.calc_open_trade_price()
|
||||||
|
close_trade_price = self.calc_close_trade_price(
|
||||||
|
rate=Decimal(rate or self.close_rate),
|
||||||
|
fee=Decimal(fee or self.fee)
|
||||||
|
)
|
||||||
|
return float("{0:.8f}".format(close_trade_price - open_trade_price))
|
||||||
|
|
||||||
|
def calc_profit_percent(
|
||||||
|
self,
|
||||||
|
rate: Optional[float] = None,
|
||||||
|
fee: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates the profit in percentage (including fee).
|
Calculates the profit in percentage (including fee).
|
||||||
:param rate: rate to compare with (optional).
|
:param rate: rate to compare with (optional).
|
||||||
@ -115,5 +187,11 @@ class Trade(_DECL_BASE):
|
|||||||
:return: profit in percentage as float
|
:return: profit in percentage as float
|
||||||
"""
|
"""
|
||||||
getcontext().prec = 8
|
getcontext().prec = 8
|
||||||
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
|
|
||||||
/ Decimal(self.open_rate) - Decimal(self.fee))
|
open_trade_price = self.calc_open_trade_price()
|
||||||
|
close_trade_price = self.calc_close_trade_price(
|
||||||
|
rate=Decimal(rate or self.close_rate),
|
||||||
|
fee=Decimal(fee or self.fee)
|
||||||
|
)
|
||||||
|
|
||||||
|
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import timedelta
|
from decimal import Decimal
|
||||||
|
from datetime import timedelta, datetime
|
||||||
from typing import Callable, Any
|
from typing import Callable, Any
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
@ -14,6 +15,7 @@ from telegram.ext import CommandHandler, Updater
|
|||||||
from freqtrade import exchange, __version__
|
from freqtrade import exchange, __version__
|
||||||
from freqtrade.misc import get_state, State, update_state
|
from freqtrade.misc import get_state, State, update_state
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
|
|
||||||
# Remove noisy log messages
|
# Remove noisy log messages
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||||
@ -22,6 +24,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
_UPDATER: Updater = None
|
_UPDATER: Updater = None
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
|
_FIAT_CONVERT = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
def init(config: dict) -> None:
|
||||||
@ -49,6 +52,7 @@ def init(config: dict) -> None:
|
|||||||
CommandHandler('stop', _stop),
|
CommandHandler('stop', _stop),
|
||||||
CommandHandler('forcesell', _forcesell),
|
CommandHandler('forcesell', _forcesell),
|
||||||
CommandHandler('performance', _performance),
|
CommandHandler('performance', _performance),
|
||||||
|
CommandHandler('daily', _daily),
|
||||||
CommandHandler('count', _count),
|
CommandHandler('count', _count),
|
||||||
CommandHandler('help', _help),
|
CommandHandler('help', _help),
|
||||||
CommandHandler('version', _version),
|
CommandHandler('version', _version),
|
||||||
@ -91,7 +95,7 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
|
|||||||
:return: decorated function
|
:return: decorated function
|
||||||
"""
|
"""
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
|
update = kwargs.get('update') or args[1]
|
||||||
|
|
||||||
# Reject unauthorized messages
|
# Reject unauthorized messages
|
||||||
chat_id = int(_CONF['telegram']['chat_id'])
|
chat_id = int(_CONF['telegram']['chat_id'])
|
||||||
@ -137,7 +141,7 @@ def _status(bot: Bot, update: Update) -> None:
|
|||||||
order = exchange.get_order(trade.open_order_id)
|
order = exchange.get_order(trade.open_order_id)
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
current_profit = trade.calc_profit(current_rate)
|
current_profit = trade.calc_profit_percent(current_rate)
|
||||||
fmt_close_profit = '{:.2f}%'.format(
|
fmt_close_profit = '{:.2f}%'.format(
|
||||||
round(trade.close_profit * 100, 2)
|
round(trade.close_profit * 100, 2)
|
||||||
) if trade.close_profit else None
|
) if trade.close_profit else None
|
||||||
@ -194,7 +198,7 @@ def _status_table(bot: Bot, update: Update) -> None:
|
|||||||
trade.id,
|
trade.id,
|
||||||
trade.pair,
|
trade.pair,
|
||||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||||
'{:.2f}'.format(100 * trade.calc_profit(current_rate))
|
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate))
|
||||||
])
|
])
|
||||||
|
|
||||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||||
@ -207,6 +211,65 @@ def _status_table(bot: Bot, update: Update) -> None:
|
|||||||
send_msg(message, parse_mode=ParseMode.HTML)
|
send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _daily(bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /daily <n>
|
||||||
|
Returns a daily profit (in BTC) over the last n days.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
today = datetime.utcnow().date()
|
||||||
|
profit_days = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
timescale = int(update.message.text.replace('/daily', '').strip())
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
timescale = 7
|
||||||
|
|
||||||
|
if not (isinstance(timescale, int) and timescale > 0):
|
||||||
|
send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot)
|
||||||
|
return
|
||||||
|
|
||||||
|
for day in range(0, timescale):
|
||||||
|
profitday = today - timedelta(days=day)
|
||||||
|
trades = Trade.query \
|
||||||
|
.filter(Trade.is_open.is_(False)) \
|
||||||
|
.filter(Trade.close_date >= profitday)\
|
||||||
|
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
|
||||||
|
.order_by(Trade.close_date)\
|
||||||
|
.all()
|
||||||
|
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||||
|
profit_days[profitday] = format(curdayprofit, '.8f')
|
||||||
|
|
||||||
|
stats = [
|
||||||
|
[
|
||||||
|
key,
|
||||||
|
'{value:.8f} {symbol}'.format(value=float(value), symbol=_CONF['stake_currency']),
|
||||||
|
'{value:.3f} {symbol}'.format(
|
||||||
|
value=_FIAT_CONVERT.convert_amount(
|
||||||
|
value,
|
||||||
|
_CONF['stake_currency'],
|
||||||
|
_CONF['fiat_display_currency']
|
||||||
|
),
|
||||||
|
symbol=_CONF['fiat_display_currency']
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for key, value in profit_days.items()
|
||||||
|
]
|
||||||
|
stats = tabulate(stats,
|
||||||
|
headers=[
|
||||||
|
'Day',
|
||||||
|
'Profit {}'.format(_CONF['stake_currency']),
|
||||||
|
'Profit {}'.format(_CONF['fiat_display_currency'])
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
|
||||||
|
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.format(timescale, stats)
|
||||||
|
send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _profit(bot: Bot, update: Update) -> None:
|
def _profit(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
@ -218,23 +281,31 @@ def _profit(bot: Bot, update: Update) -> None:
|
|||||||
"""
|
"""
|
||||||
trades = Trade.query.order_by(Trade.id).all()
|
trades = Trade.query.order_by(Trade.id).all()
|
||||||
|
|
||||||
profit_amounts = []
|
profit_all_coin = []
|
||||||
profits = []
|
profit_all_percent = []
|
||||||
|
profit_closed_coin = []
|
||||||
|
profit_closed_percent = []
|
||||||
durations = []
|
durations = []
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
current_rate = None
|
||||||
|
|
||||||
if not trade.open_rate:
|
if not trade.open_rate:
|
||||||
continue
|
continue
|
||||||
if trade.close_date:
|
if trade.close_date:
|
||||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||||
if trade.close_profit:
|
|
||||||
profit = trade.close_profit
|
if not trade.is_open:
|
||||||
|
profit_percent = trade.calc_profit_percent()
|
||||||
|
profit_closed_coin.append(trade.calc_profit())
|
||||||
|
profit_closed_percent.append(profit_percent)
|
||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
profit = trade.calc_profit(current_rate)
|
profit_percent = trade.calc_profit_percent(rate=current_rate)
|
||||||
|
|
||||||
profit_amounts.append(profit * trade.stake_amount)
|
profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
|
||||||
profits.append(profit)
|
profit_all_percent.append(profit_percent)
|
||||||
|
|
||||||
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||||
.filter(Trade.is_open.is_(False)) \
|
.filter(Trade.is_open.is_(False)) \
|
||||||
@ -247,16 +318,46 @@ def _profit(bot: Bot, update: Update) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
bp_pair, bp_rate = best_pair
|
||||||
|
|
||||||
|
# Prepare data to display
|
||||||
|
profit_closed_coin = round(sum(profit_closed_coin), 8)
|
||||||
|
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
|
||||||
|
profit_closed_fiat = _FIAT_CONVERT.convert_amount(
|
||||||
|
profit_closed_coin,
|
||||||
|
_CONF['stake_currency'],
|
||||||
|
_CONF['fiat_display_currency']
|
||||||
|
)
|
||||||
|
profit_all_coin = round(sum(profit_all_coin), 8)
|
||||||
|
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
|
||||||
|
profit_all_fiat = _FIAT_CONVERT.convert_amount(
|
||||||
|
profit_all_coin,
|
||||||
|
_CONF['stake_currency'],
|
||||||
|
_CONF['fiat_display_currency']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Message to display
|
||||||
markdown_msg = """
|
markdown_msg = """
|
||||||
*ROI:* `{profit_btc:.8f} ({profit:.2f}%)`
|
*ROI:* Close trades
|
||||||
*Trade Count:* `{trade_count}`
|
∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`
|
||||||
|
∙ `{profit_closed_fiat:.3f} {fiat}`
|
||||||
|
*ROI:* All trades
|
||||||
|
∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`
|
||||||
|
∙ `{profit_all_fiat:.3f} {fiat}`
|
||||||
|
|
||||||
|
*Total Trade Count:* `{trade_count}`
|
||||||
*First Trade opened:* `{first_trade_date}`
|
*First Trade opened:* `{first_trade_date}`
|
||||||
*Latest Trade opened:* `{latest_trade_date}`
|
*Latest Trade opened:* `{latest_trade_date}`
|
||||||
*Avg. Duration:* `{avg_duration}`
|
*Avg. Duration:* `{avg_duration}`
|
||||||
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
||||||
""".format(
|
""".format(
|
||||||
profit_btc=round(sum(profit_amounts), 8),
|
coin=_CONF['stake_currency'],
|
||||||
profit=round(sum(profits) * 100, 2),
|
fiat=_CONF['fiat_display_currency'],
|
||||||
|
profit_closed_coin=profit_closed_coin,
|
||||||
|
profit_closed_percent=profit_closed_percent,
|
||||||
|
profit_closed_fiat=profit_closed_fiat,
|
||||||
|
profit_all_coin=profit_all_coin,
|
||||||
|
profit_all_percent=profit_all_percent,
|
||||||
|
profit_all_fiat=profit_all_fiat,
|
||||||
trade_count=len(trades),
|
trade_count=len(trades),
|
||||||
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
||||||
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
||||||
@ -339,10 +440,7 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
|||||||
if trade_id == 'all':
|
if trade_id == 'all':
|
||||||
# Execute sell for all open orders
|
# Execute sell for all open orders
|
||||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
# Get current rate
|
_exec_forcesell(trade)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
|
||||||
from freqtrade.main import execute_sell
|
|
||||||
execute_sell(trade, current_rate)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Query for trade
|
# Query for trade
|
||||||
@ -354,10 +452,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
|||||||
send_msg('Invalid argument. See `/help` to view usage')
|
send_msg('Invalid argument. See `/help` to view usage')
|
||||||
logger.warning('/forcesell: Invalid argument received')
|
logger.warning('/forcesell: Invalid argument received')
|
||||||
return
|
return
|
||||||
# Get current rate
|
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
_exec_forcesell(trade)
|
||||||
from freqtrade.main import execute_sell
|
|
||||||
execute_sell(trade, current_rate)
|
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
@ -431,6 +527,7 @@ def _help(bot: Bot, update: Update) -> None:
|
|||||||
*/profit:* `Lists cumulative profit from all finished trades`
|
*/profit:* `Lists cumulative profit from all finished trades`
|
||||||
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
|
*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, regardless of profit`
|
||||||
*/performance:* `Show performance of each finished trade grouped by pair`
|
*/performance:* `Show performance of each finished trade grouped by pair`
|
||||||
|
*/daily <n>:* `Shows profit or loss per day, over the last n days`
|
||||||
*/count:* `Show number of trades running compared to allowed number of trades`
|
*/count:* `Show number of trades running compared to allowed number of trades`
|
||||||
*/balance:* `Show account balance per currency`
|
*/balance:* `Show account balance per currency`
|
||||||
*/help:* `This help message`
|
*/help:* `This help message`
|
||||||
@ -451,11 +548,11 @@ def _version(bot: Bot, update: Update) -> None:
|
|||||||
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
send_msg('*Version:* `{}`'.format(__version__), bot=bot)
|
||||||
|
|
||||||
|
|
||||||
def shorten_date(date):
|
def shorten_date(_date):
|
||||||
"""
|
"""
|
||||||
Trim the date so it fits on small screens
|
Trim the date so it fits on small screens
|
||||||
"""
|
"""
|
||||||
new_date = re.sub('seconds?', 'sec', date)
|
new_date = re.sub('seconds?', 'sec', _date)
|
||||||
new_date = re.sub('minutes?', 'min', new_date)
|
new_date = re.sub('minutes?', 'min', new_date)
|
||||||
new_date = re.sub('hours?', 'h', new_date)
|
new_date = re.sub('hours?', 'h', new_date)
|
||||||
new_date = re.sub('days?', 'd', new_date)
|
new_date = re.sub('days?', 'd', new_date)
|
||||||
@ -463,6 +560,28 @@ def shorten_date(date):
|
|||||||
return new_date
|
return new_date
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
# 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':
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current rate and execute sell
|
||||||
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
|
from freqtrade.main import execute_sell
|
||||||
|
execute_sell(trade, current_rate)
|
||||||
|
|
||||||
|
|
||||||
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||||
"""
|
"""
|
||||||
Send given markdown message
|
Send given markdown message
|
||||||
@ -476,15 +595,18 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
|||||||
|
|
||||||
bot = bot or _UPDATER.bot
|
bot = bot or _UPDATER.bot
|
||||||
|
|
||||||
keyboard = [['/status table', '/profit', '/performance', ],
|
keyboard = [['/daily', '/profit', '/balance'],
|
||||||
['/balance', '/status', '/count'],
|
['/status', '/status table', '/performance'],
|
||||||
['/start', '/stop', '/help']]
|
['/count', '/start', '/stop', '/help']]
|
||||||
|
|
||||||
reply_markup = ReplyKeyboardMarkup(keyboard)
|
reply_markup = ReplyKeyboardMarkup(keyboard)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode, reply_markup=reply_markup)
|
bot.send_message(
|
||||||
|
_CONF['telegram']['chat_id'], msg,
|
||||||
|
parse_mode=parse_mode, reply_markup=reply_markup
|
||||||
|
)
|
||||||
except NetworkError as network_err:
|
except NetworkError as network_err:
|
||||||
# Sometimes the telegram server resets the current connection,
|
# Sometimes the telegram server resets the current connection,
|
||||||
# if this is the case we send the message again.
|
# if this is the case we send the message again.
|
||||||
@ -492,6 +614,9 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
|||||||
'Got Telegram NetworkError: %s! Trying one more time.',
|
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||||
network_err.message
|
network_err.message
|
||||||
)
|
)
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode, reply_markup=reply_markup)
|
bot.send_message(
|
||||||
|
_CONF['telegram']['chat_id'], msg,
|
||||||
|
parse_mode=parse_mode, reply_markup=reply_markup
|
||||||
|
)
|
||||||
except TelegramError as telegram_err:
|
except TelegramError as telegram_err:
|
||||||
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)
|
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)
|
||||||
|
@ -15,7 +15,8 @@ def default_conf():
|
|||||||
configuration = {
|
configuration = {
|
||||||
"max_open_trades": 1,
|
"max_open_trades": 1,
|
||||||
"stake_currency": "BTC",
|
"stake_currency": "BTC",
|
||||||
"stake_amount": 0.05,
|
"stake_amount": 0.001,
|
||||||
|
"fiat_display_currency": "USD",
|
||||||
"dry_run": True,
|
"dry_run": True,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
@ -61,9 +62,27 @@ def update():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker():
|
def ticker():
|
||||||
return MagicMock(return_value={
|
return MagicMock(return_value={
|
||||||
'bid': 0.07256061,
|
'bid': 0.00001098,
|
||||||
'ask': 0.072661,
|
'ask': 0.00001099,
|
||||||
'last': 0.07256061,
|
'last': 0.00001098,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ticker_sell_up():
|
||||||
|
return MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ticker_sell_down():
|
||||||
|
return MagicMock(return_value={
|
||||||
|
'bid': 0.00001044,
|
||||||
|
'ask': 0.00001043,
|
||||||
|
'last': 0.00001044,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -104,8 +123,8 @@ def limit_buy_order():
|
|||||||
'type': 'LIMIT_BUY',
|
'type': 'LIMIT_BUY',
|
||||||
'pair': 'mocked',
|
'pair': 'mocked',
|
||||||
'opened': datetime.utcnow(),
|
'opened': datetime.utcnow(),
|
||||||
'rate': 0.07256061,
|
'rate': 0.00001099,
|
||||||
'amount': 206.43811673387373,
|
'amount': 90.99181073,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'closed': datetime.utcnow(),
|
'closed': datetime.utcnow(),
|
||||||
}
|
}
|
||||||
@ -118,8 +137,41 @@ def limit_sell_order():
|
|||||||
'type': 'LIMIT_SELL',
|
'type': 'LIMIT_SELL',
|
||||||
'pair': 'mocked',
|
'pair': 'mocked',
|
||||||
'opened': datetime.utcnow(),
|
'opened': datetime.utcnow(),
|
||||||
'rate': 0.0802134,
|
'rate': 0.00001173,
|
||||||
'amount': 206.43811673387373,
|
'amount': 90.99181073,
|
||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'closed': datetime.utcnow(),
|
'closed': datetime.utcnow(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
188
freqtrade/tests/exchange/test_exchange.py
Normal file
188
freqtrade/tests/exchange/test_exchange.py
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from requests.exceptions import RequestException
|
||||||
|
from random import randint
|
||||||
|
import logging
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade import OperationalException
|
||||||
|
from freqtrade.exchange import init, validate_pairs, buy, sell, get_balance, get_balances, \
|
||||||
|
get_ticker, cancel_order, get_name, get_fee
|
||||||
|
|
||||||
|
|
||||||
|
def test_init(default_conf, mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True)
|
||||||
|
init(config=default_conf)
|
||||||
|
assert ('freqtrade.exchange',
|
||||||
|
logging.INFO,
|
||||||
|
'Instance is running with dry_run enabled'
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_exception(default_conf, mocker):
|
||||||
|
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
OperationalException,
|
||||||
|
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
|
||||||
|
init(config=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',
|
||||||
|
])
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_pairs_not_available(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.get_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'):
|
||||||
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||||
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_pairs_exception(default_conf, mocker, caplog):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.get_markets = MagicMock(side_effect=RequestException())
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
|
# with pytest.raises(RequestException, match=r'Unable to validate pairs'):
|
||||||
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
assert ('freqtrade.exchange',
|
||||||
|
logging.WARNING,
|
||||||
|
'Unable to validate pairs (assuming they are correct). Reason: '
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def test_buy_prod(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.buy = MagicMock(return_value='dry_run_buy_{}'.format(randint(0, 10**6)))
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sell_prod(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.sell = MagicMock(return_value='dry_run_sell_{}'.format(randint(0, 10**6)))
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_balance_dry_run(default_conf, mocker):
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
|
||||||
|
assert get_balance(currency='BTC') == 999.9
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_balance_prod(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.get_balance = MagicMock(return_value=123.4)
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||||
|
|
||||||
|
assert get_balance(currency='BTC') == 123.4
|
||||||
|
|
||||||
|
|
||||||
|
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() == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_balances_prod(default_conf, mocker):
|
||||||
|
balance_item = {
|
||||||
|
'Currency': '1ST',
|
||||||
|
'Balance': 10.0,
|
||||||
|
'Available': 10.0,
|
||||||
|
'Pending': 0.0,
|
||||||
|
'CryptoAddress': None
|
||||||
|
}
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.get_balances = MagicMock(return_value=[balance_item, balance_item, 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
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_ticker(mocker, ticker):
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.get_ticker = MagicMock(return_value=ticker())
|
||||||
|
mocker.patch('freqtrade.exchange._API', api_mock)
|
||||||
|
|
||||||
|
ticker = get_ticker(pair='BTC_ETH')
|
||||||
|
assert ticker['bid'] == 0.00001098
|
||||||
|
assert ticker['ask'] == 0.00001099
|
||||||
|
assert ticker['bid'] == 0.00001098
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_name(default_conf, mocker):
|
||||||
|
mocker.patch('freqtrade.exchange.validate_pairs', side_effect=lambda s: True)
|
||||||
|
default_conf['exchange']['name'] = 'bittrex'
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
assert get_name() == 'Bittrex'
|
||||||
|
|
||||||
|
|
||||||
|
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
|
32
freqtrade/tests/exchange/test_exchange_bittrex.py
Normal file
32
freqtrade/tests/exchange/test_exchange_bittrex.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from requests.exceptions import ContentDecodingError
|
||||||
|
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
159
freqtrade/tests/optimize/test_backtesting.py
Normal file
159
freqtrade/tests/optimize/test_backtesting.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
|
|
||||||
|
import math
|
||||||
|
import pandas as pd
|
||||||
|
# from unittest.mock import MagicMock
|
||||||
|
from freqtrade import exchange, optimize
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
|
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
|
||||||
|
# import freqtrade.optimize.backtesting as backtesting
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_text_table():
|
||||||
|
results = pd.DataFrame(
|
||||||
|
{
|
||||||
|
'currency': ['BTC_ETH', 'BTC_ETH'],
|
||||||
|
'profit_percent': [0.1, 0.2],
|
||||||
|
'profit_BTC': [0.2, 0.4],
|
||||||
|
'duration': [10, 30]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert generate_text_table({'BTC_ETH': {}}, results, 'BTC', 5) == (
|
||||||
|
'pair buy count avg profit total profit avg duration\n'
|
||||||
|
'------- ----------- ------------ -------------- --------------\n'
|
||||||
|
'BTC_ETH 2 15.00% 0.60000000 BTC 100\n'
|
||||||
|
'TOTAL 2 15.00% 0.60000000 BTC 100')
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_timeframe():
|
||||||
|
data = optimize.load_data(ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||||
|
min_date, max_date = get_timeframe(data)
|
||||||
|
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
|
||||||
|
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest(default_conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH'])
|
||||||
|
results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 10, True)
|
||||||
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_1min_ticker_interval(default_conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
# Run a backtesting for an exiting 5min ticker_interval
|
||||||
|
data = optimize.load_data(ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||||
|
results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 1, True)
|
||||||
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def trim_dictlist(dl, num):
|
||||||
|
new = {}
|
||||||
|
for pair, pair_data in dl.items():
|
||||||
|
# Can't figure out why -num wont work
|
||||||
|
new[pair] = pair_data[num:]
|
||||||
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
def load_data_test(what):
|
||||||
|
data = optimize.load_data(ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||||
|
data = trim_dictlist(data, -100)
|
||||||
|
pair = data['BTC_UNITEST']
|
||||||
|
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}]
|
||||||
|
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)]}
|
||||||
|
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)]}
|
||||||
|
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
|
||||||
|
'O': math.sin(x*hz) / 1000 + base, # But replace O,H,L,C
|
||||||
|
'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 data
|
||||||
|
|
||||||
|
|
||||||
|
def simple_backtest(config, contour, num_results):
|
||||||
|
data = load_data_test(contour)
|
||||||
|
processed = optimize.preprocess(data)
|
||||||
|
assert isinstance(processed, dict)
|
||||||
|
results = backtest(config['stake_amount'], processed, 1, True)
|
||||||
|
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||||
|
assert len(results) == num_results
|
||||||
|
|
||||||
|
|
||||||
|
# Test backtest on offline data
|
||||||
|
# loaded by freqdata/optimize/__init__.py::load_data()
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest2(default_conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH'])
|
||||||
|
results = backtest(default_conf['stake_amount'], optimize.preprocess(data), 10, True)
|
||||||
|
assert not results.empty
|
||||||
|
|
||||||
|
|
||||||
|
def test_processed(default_conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
dict_of_tickerrows = load_data_test('raise')
|
||||||
|
dataframes = optimize.preprocess(dict_of_tickerrows)
|
||||||
|
dataframe = dataframes['BTC_UNITEST']
|
||||||
|
cols = dataframe.columns
|
||||||
|
# assert the dataframe got some of the indicator columns
|
||||||
|
for col in ['close', 'high', 'low', 'open', 'date',
|
||||||
|
'ema50', 'ao', 'macd', 'plus_dm']:
|
||||||
|
assert col in cols
|
||||||
|
|
||||||
|
|
||||||
|
def test_backtest_pricecontours(default_conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
tests = [['raise', 17], ['lower', 0], ['sine', 17]]
|
||||||
|
for [contour, numres] in tests:
|
||||||
|
simple_backtest(default_conf, contour, numres)
|
||||||
|
|
||||||
|
# Please make this work, the load_config needs to be mocked
|
||||||
|
# and cleanups.
|
||||||
|
# def test_backtest_start(default_conf, mocker):
|
||||||
|
# default_conf['exchange']['pair_whitelist'] = ['BTC_UNITEST']
|
||||||
|
# mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
# # see https://pypi.python.org/pypi/pytest-mock/
|
||||||
|
# # and http://www.voidspace.org.uk/python/mock/patch.html
|
||||||
|
# # No usage example of simple function mocking,
|
||||||
|
# # and no documentation of side_effect
|
||||||
|
# mocker.patch('freqtrade.misc.load_config', new=lambda s, t: {})
|
||||||
|
# args = MagicMock()
|
||||||
|
# args.level = 10
|
||||||
|
# #load_config('foo')
|
||||||
|
# backtesting.start(args)
|
||||||
|
#
|
||||||
|
# Check what sideeffect backtstesting has done.
|
||||||
|
# Probably need to capture standard-output and
|
||||||
|
# check for the generated report table.
|
79
freqtrade/tests/optimize/test_hyperopt.py
Normal file
79
freqtrade/tests/optimize/test_hyperopt.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring,W0212,C0103
|
||||||
|
|
||||||
|
from freqtrade.optimize.hyperopt import calculate_loss, TARGET_TRADES, EXPECTED_MAX_PROFIT, start, \
|
||||||
|
log_results
|
||||||
|
|
||||||
|
|
||||||
|
def test_loss_calculation_prefer_correct_trade_count():
|
||||||
|
correct = calculate_loss(1, TARGET_TRADES)
|
||||||
|
over = calculate_loss(1, TARGET_TRADES + 100)
|
||||||
|
under = calculate_loss(1, TARGET_TRADES - 100)
|
||||||
|
assert over > correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_loss_calculation_has_limited_profit():
|
||||||
|
correct = calculate_loss(EXPECTED_MAX_PROFIT, TARGET_TRADES)
|
||||||
|
over = calculate_loss(EXPECTED_MAX_PROFIT * 2, TARGET_TRADES)
|
||||||
|
under = calculate_loss(EXPECTED_MAX_PROFIT / 2, TARGET_TRADES)
|
||||||
|
assert over == correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def create_trials(mocker):
|
||||||
|
return mocker.Mock(
|
||||||
|
results=[{
|
||||||
|
'loss': 1,
|
||||||
|
'result': 'foo'
|
||||||
|
}]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_calls_fmin(mocker):
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Trials', return_value=create_trials(mocker))
|
||||||
|
mocker.patch('freqtrade.optimize.preprocess')
|
||||||
|
mocker.patch('freqtrade.optimize.load_data')
|
||||||
|
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
|
|
||||||
|
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=False)
|
||||||
|
start(args)
|
||||||
|
|
||||||
|
mock_fmin.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_uses_mongotrials(mocker):
|
||||||
|
mock_mongotrials = mocker.patch('freqtrade.optimize.hyperopt.MongoTrials',
|
||||||
|
return_value=create_trials(mocker))
|
||||||
|
mocker.patch('freqtrade.optimize.preprocess')
|
||||||
|
mocker.patch('freqtrade.optimize.load_data')
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
|
||||||
|
|
||||||
|
args = mocker.Mock(epochs=1, config='config.json.example', mongodb=True)
|
||||||
|
start(args)
|
||||||
|
|
||||||
|
mock_mongotrials.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_log_results_if_loss_improves(mocker):
|
||||||
|
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
|
||||||
|
global CURRENT_BEST_LOSS
|
||||||
|
CURRENT_BEST_LOSS = 2
|
||||||
|
log_results({
|
||||||
|
'loss': 1,
|
||||||
|
'current_tries': 1,
|
||||||
|
'total_tries': 2,
|
||||||
|
'result': 'foo'
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_log_if_loss_does_not_improve(mocker):
|
||||||
|
logger = mocker.patch('freqtrade.optimize.hyperopt.logger.info')
|
||||||
|
global CURRENT_BEST_LOSS
|
||||||
|
CURRENT_BEST_LOSS = 2
|
||||||
|
log_results({
|
||||||
|
'loss': 3,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert not logger.called
|
16
freqtrade/tests/optimize/test_hyperopt_config.py
Normal file
16
freqtrade/tests/optimize/test_hyperopt_config.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
|
|
||||||
|
from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf
|
||||||
|
|
||||||
|
|
||||||
|
def test_hyperopt_optimize_conf():
|
||||||
|
hyperopt_conf = hyperopt_optimize_conf()
|
||||||
|
|
||||||
|
assert "max_open_trades" in hyperopt_conf
|
||||||
|
assert "stake_currency" in hyperopt_conf
|
||||||
|
assert "stake_amount" in hyperopt_conf
|
||||||
|
assert "minimal_roi" in hyperopt_conf
|
||||||
|
assert "stoploss" in hyperopt_conf
|
||||||
|
assert "bid_strategy" in hyperopt_conf
|
||||||
|
assert "exchange" in hyperopt_conf
|
||||||
|
assert "pair_whitelist" in hyperopt_conf['exchange']
|
166
freqtrade/tests/optimize/test_optimize.py
Normal file
166
freqtrade/tests/optimize/test_optimize.py
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from shutil import copyfile
|
||||||
|
from freqtrade import exchange, optimize
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
|
from freqtrade.optimize.__init__ import testdata_path, download_pairs, download_backtesting_testdata
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_file(file: str, copy_file: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Backup existing file to avoid deleting the user file
|
||||||
|
:param file: complete path to the file
|
||||||
|
:param touch_file: create an empty file in replacement
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
file_swp = file + '.swp'
|
||||||
|
if os.path.isfile(file):
|
||||||
|
os.rename(file, file_swp)
|
||||||
|
|
||||||
|
if copy_file:
|
||||||
|
copyfile(file_swp, file)
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_test_file(file: str) -> None:
|
||||||
|
"""
|
||||||
|
Backup existing file to avoid deleting the user file
|
||||||
|
:param file: complete path to the file
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
file_swp = file + '.swp'
|
||||||
|
# 1. Delete file from the test
|
||||||
|
if os.path.isfile(file):
|
||||||
|
os.remove(file)
|
||||||
|
|
||||||
|
# 2. Rollback to the initial file
|
||||||
|
if os.path.isfile(file_swp):
|
||||||
|
os.rename(file_swp, file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_5min_ticker(default_conf, ticker_history, mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
file = 'freqtrade/tests/testdata/BTC_ETH-5.json'
|
||||||
|
_backup_file(file, copy_file=True)
|
||||||
|
optimize.load_data(pairs=['BTC_ETH'])
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
assert ('freqtrade.optimize',
|
||||||
|
logging.INFO,
|
||||||
|
'Download the pair: "BTC_ETH", Interval: 5 min'
|
||||||
|
) not in caplog.record_tuples
|
||||||
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_1min_ticker(default_conf, ticker_history, mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
file = 'freqtrade/tests/testdata/BTC_ETH-1.json'
|
||||||
|
_backup_file(file, copy_file=True)
|
||||||
|
optimize.load_data(ticker_interval=1, pairs=['BTC_ETH'])
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
assert ('freqtrade.optimize',
|
||||||
|
logging.INFO,
|
||||||
|
'Download the pair: "BTC_ETH", Interval: 1 min'
|
||||||
|
) not in caplog.record_tuples
|
||||||
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_data_with_new_pair_1min(default_conf, ticker_history, mocker, caplog):
|
||||||
|
mocker.patch('freqtrade.optimize.get_ticker_history', return_value=ticker_history)
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
file = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
|
_backup_file(file)
|
||||||
|
optimize.load_data(ticker_interval=1, pairs=['BTC_MEME'])
|
||||||
|
assert os.path.isfile(file) is True
|
||||||
|
assert ('freqtrade.optimize',
|
||||||
|
logging.INFO,
|
||||||
|
'Download the pair: "BTC_MEME", Interval: 1 min'
|
||||||
|
) in caplog.record_tuples
|
||||||
|
_clean_test_file(file)
|
||||||
|
|
||||||
|
|
||||||
|
def test_testdata_path():
|
||||||
|
assert os.path.join('freqtrade', 'tests', 'testdata') in testdata_path()
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_pairs(default_conf, ticker_history, mocker):
|
||||||
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
_backup_file(file1_1)
|
||||||
|
_backup_file(file1_5)
|
||||||
|
_backup_file(file2_1)
|
||||||
|
_backup_file(file2_5)
|
||||||
|
|
||||||
|
assert download_pairs(pairs=['BTC-MEME', 'BTC-CFI']) is True
|
||||||
|
|
||||||
|
assert os.path.isfile(file1_1) is True
|
||||||
|
assert os.path.isfile(file1_5) is True
|
||||||
|
assert os.path.isfile(file2_1) is True
|
||||||
|
assert os.path.isfile(file2_5) is True
|
||||||
|
|
||||||
|
# clean files freshly downloaded
|
||||||
|
_clean_test_file(file1_1)
|
||||||
|
_clean_test_file(file1_5)
|
||||||
|
_clean_test_file(file2_1)
|
||||||
|
_clean_test_file(file2_5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_pairs_exception(default_conf, ticker_history, mocker, caplog):
|
||||||
|
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'))
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
file1_1 = 'freqtrade/tests/testdata/BTC_MEME-1.json'
|
||||||
|
file1_5 = 'freqtrade/tests/testdata/BTC_MEME-5.json'
|
||||||
|
_backup_file(file1_1)
|
||||||
|
_backup_file(file1_5)
|
||||||
|
|
||||||
|
download_pairs(pairs=['BTC-MEME'])
|
||||||
|
# clean files freshly downloaded
|
||||||
|
_clean_test_file(file1_1)
|
||||||
|
_clean_test_file(file1_5)
|
||||||
|
assert ('freqtrade.optimize.__init__',
|
||||||
|
logging.INFO,
|
||||||
|
'Failed to download the pair: "BTC-MEME", Interval: 1 min'
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_backtesting_testdata(default_conf, ticker_history, mocker):
|
||||||
|
mocker.patch('freqtrade.optimize.__init__.get_ticker_history', return_value=ticker_history)
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
|
# Download a 1 min ticker file
|
||||||
|
file1 = 'freqtrade/tests/testdata/BTC_XEL-1.json'
|
||||||
|
_backup_file(file1)
|
||||||
|
download_backtesting_testdata(pair="BTC-XEL", interval=1)
|
||||||
|
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'
|
||||||
|
_backup_file(file2)
|
||||||
|
|
||||||
|
download_backtesting_testdata(pair="BTC-STORJ", interval=5)
|
||||||
|
assert os.path.isfile(file2) is True
|
||||||
|
_clean_test_file(file2)
|
@ -14,7 +14,8 @@ from freqtrade.misc import update_state, State, get_state
|
|||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
from freqtrade.rpc import telegram
|
||||||
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
|
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
|
||||||
_profit, _forcesell, _performance, _count, _start, _stop, _balance, _version, _help
|
_profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help, \
|
||||||
|
_exec_forcesell
|
||||||
|
|
||||||
|
|
||||||
def test_is_enabled(default_conf, mocker):
|
def test_is_enabled(default_conf, mocker):
|
||||||
@ -101,7 +102,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
# Trigger status while we have a fulfilled order for the open trade
|
# Trigger status while we have a fulfilled order for the open trade
|
||||||
_status(bot=MagicMock(), update=update)
|
_status(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
@ -150,7 +151,8 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
|||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_profit_handle(
|
||||||
|
default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
@ -162,6 +164,9 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
_profit(bot=MagicMock(), update=update)
|
_profit(bot=MagicMock(), update=update)
|
||||||
@ -170,7 +175,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
@ -181,7 +186,10 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Update the ticker with a market going up
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
@ -189,11 +197,17 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
|
|
||||||
_profit(bot=MagicMock(), update=update)
|
_profit(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert '*ROI:* `1.50701325 (10.05%)`' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0]
|
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 '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
@ -204,10 +218,55 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
|||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Increase the price and sell it
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up)
|
||||||
|
|
||||||
|
update.message.text = '/forcesell 1'
|
||||||
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
|
assert rpc_mock.call_count == 2
|
||||||
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert '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):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
|
_CONF=default_conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker)
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
create_trade(0.001)
|
||||||
|
|
||||||
|
# Decrease the price and sell it
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_down)
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
@ -217,7 +276,33 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
|||||||
|
|
||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0]
|
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert 'profit: ~-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_exec_forcesell_open_orders(default_conf, ticker, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_order=MagicMock(return_value={
|
||||||
|
'closed': None,
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
}),
|
||||||
|
cancel_order=cancel_order_mock)
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
open_rate=1,
|
||||||
|
exchange='BITTREX',
|
||||||
|
open_order_id='123456789',
|
||||||
|
amount=1,
|
||||||
|
fee=0.0,
|
||||||
|
)
|
||||||
|
_exec_forcesell(trade)
|
||||||
|
|
||||||
|
assert cancel_order_mock.call_count == 1
|
||||||
|
assert trade.is_open is False
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||||
@ -231,11 +316,14 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
|||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
rpc_mock.reset_mock()
|
rpc_mock.reset_mock()
|
||||||
|
|
||||||
update.message.text = '/forcesell all'
|
update.message.text = '/forcesell all'
|
||||||
@ -243,7 +331,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
|||||||
|
|
||||||
assert rpc_mock.call_count == 4
|
assert rpc_mock.call_count == 4
|
||||||
for args in rpc_mock.call_args_list:
|
for args in rpc_mock.call_args_list:
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
assert '0.00001098' in args[0][0]
|
||||||
|
assert 'profit: ~-0.59%, -0.00000591 BTC' in args[0][0]
|
||||||
|
assert '-0.089 USD' in args[0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||||
@ -298,7 +388,7 @@ def test_performance_handle(
|
|||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -314,7 +404,57 @@ def test_performance_handle(
|
|||||||
_performance(bot=MagicMock(), update=update)
|
_performance(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>BTC_ETH\t6.20%</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_daily_handle(
|
||||||
|
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
|
_CONF=default_conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker)
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
create_trade(0.001)
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.is_open = False
|
||||||
|
|
||||||
|
# Try valid data
|
||||||
|
update.message.text = '/daily 2'
|
||||||
|
_daily(bot=MagicMock(), update=update)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'Daily' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
# Try invalid data
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
update_state(State.RUNNING)
|
||||||
|
update.message.text = '/daily -2'
|
||||||
|
_daily(bot=MagicMock(), update=update)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, mocker):
|
def test_count_handle(default_conf, update, ticker, mocker):
|
||||||
@ -339,7 +479,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
|||||||
update_state(State.RUNNING)
|
update_state(State.RUNNING)
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
_count(bot=MagicMock(), update=update)
|
_count(bot=MagicMock(), update=update)
|
||||||
|
|
71
freqtrade/tests/test_acl_pair.py
Normal file
71
freqtrade/tests/test_acl_pair.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
from freqtrade.main import refresh_whitelist
|
||||||
|
|
||||||
|
# whitelist, blacklist, filtering, all of that will
|
||||||
|
# eventually become some rules to run on a generic ACL engine
|
||||||
|
# perhaps try to anticipate that by using some python package
|
||||||
|
|
||||||
|
|
||||||
|
def whitelist_conf():
|
||||||
|
return {
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"exchange": {
|
||||||
|
"pair_whitelist": [
|
||||||
|
"BTC_ETH",
|
||||||
|
"BTC_TKN",
|
||||||
|
"BTC_TRST",
|
||||||
|
"BTC_SWT",
|
||||||
|
"BTC_BCC"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_health():
|
||||||
|
return [{'Currency': 'ETH',
|
||||||
|
'IsActive': True
|
||||||
|
},
|
||||||
|
{'Currency': 'TKN',
|
||||||
|
'IsActive': True
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
def get_health_empty():
|
||||||
|
return []
|
||||||
|
|
||||||
|
# below three test could be merged into a single
|
||||||
|
# test that ran randomlly generated health lists
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_whitelist(mocker):
|
||||||
|
conf = whitelist_conf()
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
get_wallet_health=get_health)
|
||||||
|
refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist'])
|
||||||
|
whitelist = ['BTC_ETH', 'BTC_TKN']
|
||||||
|
# Ensure all except those in whitelist are removed
|
||||||
|
assert set(whitelist) == set(refreshedwhitelist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_whitelist_dynamic(mocker):
|
||||||
|
conf = whitelist_conf()
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
get_wallet_health=get_health)
|
||||||
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
|
whitelist = ['BTC_ETH', 'BTC_TKN']
|
||||||
|
refreshedwhitelist = refresh_whitelist(whitelist)
|
||||||
|
assert set(whitelist) == set(refreshedwhitelist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_whitelist_dynamic_empty(mocker):
|
||||||
|
conf = whitelist_conf()
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
get_wallet_health=get_health_empty)
|
||||||
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
|
whitelist = []
|
||||||
|
conf['exchange']['pair_whitelist'] = []
|
||||||
|
refresh_whitelist(whitelist)
|
||||||
|
pairslist = conf['exchange']['pair_whitelist']
|
||||||
|
assert set(whitelist) == set(pairslist)
|
@ -22,7 +22,7 @@ def test_dataframe_correct_columns(result):
|
|||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
assert len(result.index) == 14382
|
assert len(result.index) == 14395
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
|
27
freqtrade/tests/test_dataframe.py
Normal file
27
freqtrade/tests/test_dataframe.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import pandas
|
||||||
|
|
||||||
|
from freqtrade import analyze
|
||||||
|
import freqtrade.optimize
|
||||||
|
|
||||||
|
_pairs = ['BTC_ETH']
|
||||||
|
|
||||||
|
|
||||||
|
def load_dataframe_pair(pairs):
|
||||||
|
ld = freqtrade.optimize.load_data(ticker_interval=5, pairs=pairs)
|
||||||
|
assert isinstance(ld, dict)
|
||||||
|
assert isinstance(pairs[0], str)
|
||||||
|
dataframe = ld[pairs[0]]
|
||||||
|
dataframe = analyze.analyze_ticker(dataframe)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataframe_load():
|
||||||
|
dataframe = load_dataframe_pair(_pairs)
|
||||||
|
assert isinstance(dataframe, pandas.core.frame.DataFrame)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dataframe_columns_exists():
|
||||||
|
dataframe = load_dataframe_pair(_pairs)
|
||||||
|
assert 'high' in dataframe.columns
|
||||||
|
assert 'low' in dataframe.columns
|
||||||
|
assert 'close' in dataframe.columns
|
@ -1,36 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring,C0103
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
|
||||||
from freqtrade.exchange import validate_pairs
|
|
||||||
|
|
||||||
|
|
||||||
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',
|
|
||||||
])
|
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_not_available(default_conf, mocker):
|
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.get_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'):
|
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
|
|
||||||
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'
|
|
||||||
mocker.patch('freqtrade.exchange._API', api_mock)
|
|
||||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
|
||||||
with pytest.raises(OperationalException, match=r'not compatible'):
|
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
|
111
freqtrade/tests/test_fiat_convert.py
Normal file
111
freqtrade/tests/test_fiat_convert.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
|
|
||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from freqtrade.fiat_convert import CryptoToFiatConverter, CryptoFiat
|
||||||
|
|
||||||
|
|
||||||
|
def test_pair_convertion_object():
|
||||||
|
pair_convertion = CryptoFiat(
|
||||||
|
crypto_symbol='btc',
|
||||||
|
fiat_symbol='usd',
|
||||||
|
price=12345.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the cache duration is 6 hours
|
||||||
|
assert pair_convertion.CACHE_DURATION == 6 * 60 * 60
|
||||||
|
|
||||||
|
# Check a regular usage
|
||||||
|
assert pair_convertion.crypto_symbol == 'BTC'
|
||||||
|
assert pair_convertion.fiat_symbol == 'USD'
|
||||||
|
assert pair_convertion.price == 12345.0
|
||||||
|
assert pair_convertion.is_expired() is False
|
||||||
|
|
||||||
|
# Update the expiration time (- 2 hours) and check the behavior
|
||||||
|
pair_convertion._expiration = time.time() - 2 * 60 * 60
|
||||||
|
assert pair_convertion.is_expired() is True
|
||||||
|
|
||||||
|
# Check set price behaviour
|
||||||
|
time_reference = time.time() + pair_convertion.CACHE_DURATION
|
||||||
|
pair_convertion.set_price(price=30000.123)
|
||||||
|
assert pair_convertion.is_expired() is False
|
||||||
|
assert pair_convertion._expiration >= time_reference
|
||||||
|
assert pair_convertion.price == 30000.123
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_is_supported():
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
assert fiat_convert._is_supported_fiat(fiat='USD') is True
|
||||||
|
assert fiat_convert._is_supported_fiat(fiat='usd') is True
|
||||||
|
assert fiat_convert._is_supported_fiat(fiat='abc') is False
|
||||||
|
assert fiat_convert._is_supported_fiat(fiat='ABC') is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_add_pair():
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
assert len(fiat_convert._pairs) == 0
|
||||||
|
|
||||||
|
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='usd', price=12345.0)
|
||||||
|
assert len(fiat_convert._pairs) == 1
|
||||||
|
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
||||||
|
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
||||||
|
assert fiat_convert._pairs[0].price == 12345.0
|
||||||
|
|
||||||
|
fiat_convert._add_pair(crypto_symbol='btc', fiat_symbol='Eur', price=13000.2)
|
||||||
|
assert len(fiat_convert._pairs) == 2
|
||||||
|
assert fiat_convert._pairs[1].crypto_symbol == 'BTC'
|
||||||
|
assert fiat_convert._pairs[1].fiat_symbol == 'EUR'
|
||||||
|
assert fiat_convert._pairs[1].price == 13000.2
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_find_price(mocker):
|
||||||
|
api_mock = MagicMock(return_value={
|
||||||
|
'price_usd': 12345.0,
|
||||||
|
'price_eur': 13000.2
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'):
|
||||||
|
fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='ABC')
|
||||||
|
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 12345.0
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='btc', fiat_symbol='usd') == 12345.0
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
|
||||||
|
|
||||||
|
|
||||||
|
def test_fiat_convert_get_price(mocker):
|
||||||
|
api_mock = MagicMock(return_value={
|
||||||
|
'price_usd': 28000.0,
|
||||||
|
'price_eur': 15000.0
|
||||||
|
})
|
||||||
|
mocker.patch('freqtrade.fiat_convert.Pymarketcap.ticker', api_mock)
|
||||||
|
|
||||||
|
fiat_convert = CryptoToFiatConverter()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match=r'The fiat US DOLLAR is not supported.'):
|
||||||
|
fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='US Dollar')
|
||||||
|
|
||||||
|
# Check the value return by the method
|
||||||
|
assert len(fiat_convert._pairs) == 0
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
|
||||||
|
assert fiat_convert._pairs[0].crypto_symbol == 'BTC'
|
||||||
|
assert fiat_convert._pairs[0].fiat_symbol == 'USD'
|
||||||
|
assert fiat_convert._pairs[0].price == 28000.0
|
||||||
|
assert fiat_convert._pairs[0]._expiration is not 0
|
||||||
|
assert len(fiat_convert._pairs) == 1
|
||||||
|
|
||||||
|
# Verify the cached is used
|
||||||
|
fiat_convert._pairs[0].price = 9867.543
|
||||||
|
expiration = fiat_convert._pairs[0]._expiration
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 9867.543
|
||||||
|
assert fiat_convert._pairs[0]._expiration == expiration
|
||||||
|
|
||||||
|
# Verify the cache expiration
|
||||||
|
expiration = time.time() - 2 * 60 * 60
|
||||||
|
fiat_convert._pairs[0]._expiration = expiration
|
||||||
|
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='USD') == 28000.0
|
||||||
|
assert fiat_convert._pairs[0]._expiration is not expiration
|
@ -4,13 +4,14 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
|
import logging
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade import DependencyException, OperationalException
|
from freqtrade import DependencyException, OperationalException
|
||||||
from freqtrade.analyze import SignalType
|
from freqtrade.analyze import SignalType
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.main import create_trade, handle_trade, init, \
|
from freqtrade.main import create_trade, handle_trade, init, \
|
||||||
get_target_bid, _process
|
get_target_bid, _process, execute_sell
|
||||||
from freqtrade.misc import get_state, State
|
from freqtrade.misc import get_state, State
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
@ -40,8 +41,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
|
|||||||
assert trade.is_open
|
assert trade.is_open
|
||||||
assert trade.open_date is not None
|
assert trade.open_date is not None
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
assert trade.exchange == Exchanges.BITTREX.name
|
||||||
assert trade.open_rate == 0.072661
|
assert trade.open_rate == 0.00001099
|
||||||
assert trade.amount == 0.6881270557795791
|
assert trade.amount == 90.99181073703367
|
||||||
|
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
||||||
@ -115,11 +116,11 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|||||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade is not None
|
assert trade is not None
|
||||||
assert trade.stake_amount == 15.0
|
assert trade.stake_amount == 0.001
|
||||||
assert trade.is_open
|
assert trade.is_open
|
||||||
assert trade.open_date is not None
|
assert trade.open_date is not None
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
assert trade.exchange == Exchanges.BITTREX.name
|
||||||
@ -127,8 +128,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
assert trade.open_rate == 0.07256061
|
assert trade.open_rate == 0.00001099
|
||||||
assert trade.amount == 206.43811673387373
|
assert trade.amount == 90.99181073
|
||||||
|
|
||||||
assert whitelist == default_conf['exchange']['pair_whitelist']
|
assert whitelist == default_conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
@ -137,7 +138,9 @@ def test_create_trade_minimal_amount(default_conf, ticker, mocker):
|
|||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
buy_mock = mocker.patch('freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy'))
|
buy_mock = mocker.patch(
|
||||||
|
'freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy')
|
||||||
|
)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
@ -177,6 +180,23 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
|||||||
create_trade(default_conf['stake_amount'])
|
create_trade(default_conf['stake_amount'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker,
|
||||||
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
|
||||||
|
with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
|
||||||
|
conf = copy.deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'] = ["BTC_ETH"]
|
||||||
|
conf['exchange']['pair_blacklist'] = ["BTC_ETH"]
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
create_trade(default_conf['stake_amount'])
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
@ -184,14 +204,17 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.17256061,
|
'bid': 0.00001172,
|
||||||
'ask': 0.172661,
|
'ask': 0.00001173,
|
||||||
'last': 0.17256061
|
'last': 0.00001172
|
||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||||
sell=MagicMock(return_value='mocked_limit_sell'))
|
sell=MagicMock(return_value='mocked_limit_sell'))
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
@ -205,11 +228,72 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
|
||||||
assert trade.close_rate == 0.0802134
|
assert trade.close_rate == 0.00001173
|
||||||
assert trade.close_profit == 0.10046755
|
assert trade.close_profit == 0.06201057
|
||||||
|
assert trade.calc_profit() == 0.00006217
|
||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_trade_roi(default_conf, ticker, limit_buy_order, mocker, caplog):
|
||||||
|
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker,
|
||||||
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
mocker.patch('freqtrade.main.min_roi_reached', return_value=True)
|
||||||
|
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
create_trade(0.001)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
|
||||||
|
# FIX: sniffing logs, suggest handle_trade should not execute_sell
|
||||||
|
# instead that responsibility should be moved out of handle_trade(),
|
||||||
|
# we might just want to check if we are in a sell condition without
|
||||||
|
# executing
|
||||||
|
# if ROI is reached we must sell
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
|
||||||
|
assert handle_trade(trade)
|
||||||
|
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
|
||||||
|
# if ROI is reached we must sell even if sell-signal is not signalled
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
assert handle_trade(trade)
|
||||||
|
assert ('freqtrade', logging.DEBUG, 'Executing sell due to ROI ...') in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_trade_experimental(default_conf, ticker, limit_buy_order, mocker, caplog):
|
||||||
|
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker,
|
||||||
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
mocker.patch('freqtrade.main.min_roi_reached', return_value=False)
|
||||||
|
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
create_trade(0.001)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
trade.is_open = True
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: False)
|
||||||
|
value_returned = handle_trade(trade)
|
||||||
|
assert ('freqtrade', logging.DEBUG, 'Checking sell_signal ...') in caplog.record_tuples
|
||||||
|
assert value_returned is False
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
assert handle_trade(trade)
|
||||||
|
s = 'Executing sell due to sell signal ...'
|
||||||
|
assert ('freqtrade', logging.DEBUG, s) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
@ -221,7 +305,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
|
|||||||
|
|
||||||
# Create trade and sell it
|
# Create trade and sell it
|
||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
create_trade(15.0)
|
create_trade(0.001)
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
assert trade
|
assert trade
|
||||||
@ -247,3 +331,104 @@ def test_balance_fully_last_side(mocker):
|
|||||||
def test_balance_bigger_last_ask(mocker):
|
def test_balance_bigger_last_ask(mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||||
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker)
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
create_trade(0.001)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Increase the price and sell it
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up)
|
||||||
|
|
||||||
|
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
||||||
|
|
||||||
|
assert rpc_mock.call_count == 2
|
||||||
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert '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_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||||
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
|
_CONF=default_conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker)
|
||||||
|
mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap',
|
||||||
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
_cache_symbols=MagicMock(return_value={'BTC': 1}))
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
create_trade(0.001)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Decrease the price and sell it
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_down)
|
||||||
|
|
||||||
|
execute_sell(trade=trade, limit=ticker_sell_down()['bid'])
|
||||||
|
|
||||||
|
assert rpc_mock.call_count == 2
|
||||||
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert '0.00001044' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert 'profit: ~-5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_execute_sell_without_conf(default_conf, ticker, ticker_sell_up, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
|
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||||
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker)
|
||||||
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
create_trade(0.001)
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Increase the price and sell it
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=ticker_sell_up)
|
||||||
|
mocker.patch('freqtrade.main._CONF', {})
|
||||||
|
|
||||||
|
execute_sell(trade=trade, limit=ticker_sell_up()['bid'])
|
||||||
|
|
||||||
|
assert rpc_mock.call_count == 2
|
||||||
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
assert '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]
|
||||||
|
@ -16,21 +16,33 @@ def test_throttle():
|
|||||||
return 42
|
return 42
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
result = throttle(func, 0.1)
|
result = throttle(func, min_secs=0.1)
|
||||||
end = time.time()
|
end = time.time()
|
||||||
|
|
||||||
assert result == 42
|
assert result == 42
|
||||||
assert end - start > 0.1
|
assert end - start > 0.1
|
||||||
|
|
||||||
result = throttle(func, -1)
|
result = throttle(func, min_secs=-1)
|
||||||
assert result == 42
|
assert result == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_throttle_with_assets():
|
||||||
|
|
||||||
|
def func(nb_assets=-1):
|
||||||
|
return nb_assets
|
||||||
|
|
||||||
|
result = throttle(func, min_secs=0.1, nb_assets=666)
|
||||||
|
assert result == 666
|
||||||
|
|
||||||
|
result = throttle(func, min_secs=0.1)
|
||||||
|
assert result == -1
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_defaults():
|
def test_parse_args_defaults():
|
||||||
args = parse_args([])
|
args = parse_args([])
|
||||||
assert args is not None
|
assert args is not None
|
||||||
assert args.config == 'config.json'
|
assert args.config == 'config.json'
|
||||||
assert args.dynamic_whitelist is False
|
assert args.dynamic_whitelist is None
|
||||||
assert args.loglevel == 20
|
assert args.loglevel == 20
|
||||||
|
|
||||||
|
|
||||||
@ -58,11 +70,23 @@ def test_parse_args_verbose():
|
|||||||
def test_parse_args_dynamic_whitelist():
|
def test_parse_args_dynamic_whitelist():
|
||||||
args = parse_args(['--dynamic-whitelist'])
|
args = parse_args(['--dynamic-whitelist'])
|
||||||
assert args is not None
|
assert args is not None
|
||||||
assert args.dynamic_whitelist is True
|
assert args.dynamic_whitelist is 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist_10():
|
||||||
|
args = parse_args(['--dynamic-whitelist', '10'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.dynamic_whitelist is 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist_invalid_values():
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['--dynamic-whitelist', 'abc'])
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting(mocker):
|
def test_parse_args_backtesting(mocker):
|
||||||
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
|
backtesting_mock = mocker.patch(
|
||||||
|
'freqtrade.optimize.backtesting.start', MagicMock())
|
||||||
args = parse_args(['backtesting'])
|
args = parse_args(['backtesting'])
|
||||||
assert args is None
|
assert args is None
|
||||||
assert backtesting_mock.call_count == 1
|
assert backtesting_mock.call_count == 1
|
||||||
@ -85,8 +109,14 @@ def test_parse_args_backtesting_invalid():
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_args_backtesting_custom(mocker):
|
def test_parse_args_backtesting_custom(mocker):
|
||||||
backtesting_mock = mocker.patch('freqtrade.optimize.backtesting.start', MagicMock())
|
backtesting_mock = mocker.patch(
|
||||||
args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1'])
|
'freqtrade.optimize.backtesting.start', MagicMock())
|
||||||
|
args = parse_args([
|
||||||
|
'-c', 'test_conf.json',
|
||||||
|
'backtesting',
|
||||||
|
'--live',
|
||||||
|
'--ticker-interval', '1',
|
||||||
|
'--refresh-pairs-cached'])
|
||||||
assert args is None
|
assert args is None
|
||||||
assert backtesting_mock.call_count == 1
|
assert backtesting_mock.call_count == 1
|
||||||
|
|
||||||
@ -97,10 +127,12 @@ def test_parse_args_backtesting_custom(mocker):
|
|||||||
assert call_args.subparser == 'backtesting'
|
assert call_args.subparser == 'backtesting'
|
||||||
assert call_args.func is not None
|
assert call_args.func is not None
|
||||||
assert call_args.ticker_interval == 1
|
assert call_args.ticker_interval == 1
|
||||||
|
assert call_args.refresh_pairs is True
|
||||||
|
|
||||||
|
|
||||||
def test_parse_args_hyperopt(mocker):
|
def test_parse_args_hyperopt(mocker):
|
||||||
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
|
hyperopt_mock = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.start', MagicMock())
|
||||||
args = parse_args(['hyperopt'])
|
args = parse_args(['hyperopt'])
|
||||||
assert args is None
|
assert args is None
|
||||||
assert hyperopt_mock.call_count == 1
|
assert hyperopt_mock.call_count == 1
|
||||||
@ -113,7 +145,8 @@ def test_parse_args_hyperopt(mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_parse_args_hyperopt_custom(mocker):
|
def test_parse_args_hyperopt_custom(mocker):
|
||||||
hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock())
|
hyperopt_mock = mocker.patch(
|
||||||
|
'freqtrade.optimize.hyperopt.start', MagicMock())
|
||||||
args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20'])
|
args = parse_args(['-c', 'test_conf.json', 'hyperopt', '--epochs', '20'])
|
||||||
assert args is None
|
assert args is None
|
||||||
assert hyperopt_mock.call_count == 1
|
assert hyperopt_mock.call_count == 1
|
||||||
@ -138,7 +171,10 @@ def test_load_config(default_conf, mocker):
|
|||||||
def test_load_config_invalid_pair(default_conf, mocker):
|
def test_load_config_invalid_pair(default_conf, mocker):
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
||||||
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
mocker.patch(
|
||||||
|
'freqtrade.misc.open',
|
||||||
|
mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)))
|
||||||
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
load_config('somefile')
|
load_config('somefile')
|
||||||
|
|
||||||
@ -146,6 +182,9 @@ def test_load_config_invalid_pair(default_conf, mocker):
|
|||||||
def test_load_config_missing_attributes(default_conf, mocker):
|
def test_load_config_missing_attributes(default_conf, mocker):
|
||||||
conf = deepcopy(default_conf)
|
conf = deepcopy(default_conf)
|
||||||
conf.pop('exchange')
|
conf.pop('exchange')
|
||||||
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
mocker.patch(
|
||||||
|
'freqtrade.misc.open',
|
||||||
|
mocker.mock_open(
|
||||||
|
read_data=json.dumps(conf)))
|
||||||
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
load_config('somefile')
|
load_config('somefile')
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
|
||||||
|
|
||||||
|
|
||||||
from freqtrade import exchange, optimize
|
|
||||||
from freqtrade.exchange import Bittrex
|
|
||||||
from freqtrade.optimize.backtesting import backtest
|
|
||||||
|
|
||||||
|
|
||||||
def test_backtest(default_conf, mocker):
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
||||||
|
|
||||||
data = optimize.load_data(ticker_interval=5, pairs=['BTC_ETH'])
|
|
||||||
results = backtest(default_conf, optimize.preprocess(data), 10, True)
|
|
||||||
num_resutls = len(results)
|
|
||||||
assert num_resutls > 0
|
|
@ -1,6 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring,W0212
|
|
||||||
|
|
||||||
|
|
||||||
def test_optimizer(default_conf, mocker):
|
|
||||||
# TODO: implement test
|
|
||||||
pass
|
|
@ -1,15 +1,125 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import os
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import init, Trade
|
||||||
|
|
||||||
|
|
||||||
def test_update(limit_buy_order, limit_sell_order):
|
def test_init_create_session(default_conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
||||||
|
|
||||||
|
# Check if init create a session
|
||||||
|
init(default_conf)
|
||||||
|
assert hasattr(Trade, 'session')
|
||||||
|
assert type(Trade.session).__name__ is 'Session'
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_dry_run_db(default_conf, mocker):
|
||||||
|
default_conf.update({'dry_run_db': True})
|
||||||
|
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
||||||
|
|
||||||
|
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
||||||
|
dry_run_db = 'tradesv3.dry_run.sqlite'
|
||||||
|
dry_run_db_swp = dry_run_db + '.swp'
|
||||||
|
|
||||||
|
if os.path.isfile(dry_run_db):
|
||||||
|
os.rename(dry_run_db, dry_run_db_swp)
|
||||||
|
|
||||||
|
# Check if the new tradesv3.dry_run.sqlite was created
|
||||||
|
init(default_conf)
|
||||||
|
assert os.path.isfile(dry_run_db) is True
|
||||||
|
|
||||||
|
# Delete the file made for this unitest and rollback to the previous
|
||||||
|
# tradesv3.dry_run.sqlite file
|
||||||
|
|
||||||
|
# 1. Delete file from the test
|
||||||
|
if os.path.isfile(dry_run_db):
|
||||||
|
os.remove(dry_run_db)
|
||||||
|
|
||||||
|
# 2. Rollback to the initial file
|
||||||
|
if os.path.isfile(dry_run_db_swp):
|
||||||
|
os.rename(dry_run_db_swp, dry_run_db)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_dry_run_without_db(default_conf, mocker):
|
||||||
|
default_conf.update({'dry_run_db': False})
|
||||||
|
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
||||||
|
|
||||||
|
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
|
||||||
|
dry_run_db = 'tradesv3.dry_run.sqlite'
|
||||||
|
dry_run_db_swp = dry_run_db + '.swp'
|
||||||
|
|
||||||
|
if os.path.isfile(dry_run_db):
|
||||||
|
os.rename(dry_run_db, dry_run_db_swp)
|
||||||
|
|
||||||
|
# Check if the new tradesv3.dry_run.sqlite was created
|
||||||
|
init(default_conf)
|
||||||
|
assert os.path.isfile(dry_run_db) is False
|
||||||
|
|
||||||
|
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
|
||||||
|
if os.path.isfile(dry_run_db_swp):
|
||||||
|
os.rename(dry_run_db_swp, dry_run_db)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_prod_db(default_conf, mocker):
|
||||||
|
default_conf.update({'dry_run': False})
|
||||||
|
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
|
||||||
|
|
||||||
|
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
|
||||||
|
prod_db = 'tradesv3.sqlite'
|
||||||
|
prod_db_swp = prod_db + '.swp'
|
||||||
|
|
||||||
|
if os.path.isfile(prod_db):
|
||||||
|
os.rename(prod_db, prod_db_swp)
|
||||||
|
|
||||||
|
# Check if the new tradesv3.sqlite was created
|
||||||
|
init(default_conf)
|
||||||
|
assert os.path.isfile(prod_db) is True
|
||||||
|
|
||||||
|
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
|
||||||
|
|
||||||
|
# 1. Delete file from the test
|
||||||
|
if os.path.isfile(prod_db):
|
||||||
|
os.remove(prod_db)
|
||||||
|
|
||||||
|
# Rollback to the initial 'tradesv3.sqlite' file
|
||||||
|
if os.path.isfile(prod_db_swp):
|
||||||
|
os.rename(prod_db_swp, prod_db)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_with_bittrex(limit_buy_order, limit_sell_order):
|
||||||
|
"""
|
||||||
|
On this test we will buy and sell a crypto currency.
|
||||||
|
|
||||||
|
Buy
|
||||||
|
- Buy: 90.99181073 Crypto at 0.00001099 BTC
|
||||||
|
(90.99181073*0.00001099 = 0.0009999 BTC)
|
||||||
|
- Buying fee: 0.25%
|
||||||
|
- Total cost of buy trade: 0.001002500 BTC
|
||||||
|
((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025))
|
||||||
|
|
||||||
|
Sell
|
||||||
|
- Sell: 90.99181073 Crypto at 0.00001173 BTC
|
||||||
|
(90.99181073*0.00001173 = 0,00106733394 BTC)
|
||||||
|
- Selling fee: 0.25%
|
||||||
|
- Total cost of sell trade: 0.001064666 BTC
|
||||||
|
((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025))
|
||||||
|
|
||||||
|
Profit/Loss: +0.000062166 BTC
|
||||||
|
(Sell:0.001064666 - Buy:0.001002500)
|
||||||
|
Profit/Loss percentage: 0.0620
|
||||||
|
((0.001064666/0.001002500)-1 = 6.20%)
|
||||||
|
|
||||||
|
:param limit_buy_order:
|
||||||
|
:param limit_sell_order:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
stake_amount=1.00,
|
stake_amount=0.001,
|
||||||
fee=0.1,
|
fee=0.0025,
|
||||||
exchange=Exchanges.BITTREX,
|
exchange=Exchanges.BITTREX,
|
||||||
)
|
)
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
@ -20,18 +130,53 @@ def test_update(limit_buy_order, limit_sell_order):
|
|||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
assert trade.open_rate == 0.07256061
|
assert trade.open_rate == 0.00001099
|
||||||
assert trade.close_profit is None
|
assert trade.close_profit is None
|
||||||
assert trade.close_date is None
|
assert trade.close_date is None
|
||||||
|
|
||||||
trade.open_order_id = 'something'
|
trade.open_order_id = 'something'
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
assert trade.open_order_id is None
|
assert trade.open_order_id is None
|
||||||
assert trade.open_rate == 0.07256061
|
assert trade.close_rate == 0.00001173
|
||||||
assert trade.close_profit == 0.00546755
|
assert trade.close_profit == 0.06201057
|
||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee=0.0025,
|
||||||
|
exchange=Exchanges.BITTREX,
|
||||||
|
)
|
||||||
|
|
||||||
|
trade.open_order_id = 'something'
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
assert trade.calc_open_trade_price() == 0.001002500
|
||||||
|
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
assert trade.calc_close_trade_price() == 0.0010646656
|
||||||
|
|
||||||
|
# Profit in BTC
|
||||||
|
assert trade.calc_profit() == 0.00006217
|
||||||
|
|
||||||
|
# Profit in percent
|
||||||
|
assert trade.calc_profit_percent() == 0.06201057
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_close_trade_price_exception(limit_buy_order):
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee=0.0025,
|
||||||
|
exchange=Exchanges.BITTREX,
|
||||||
|
)
|
||||||
|
|
||||||
|
trade.open_order_id = 'something'
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
assert trade.calc_close_trade_price() == 0.0
|
||||||
|
|
||||||
|
|
||||||
def test_update_open_order(limit_buy_order):
|
def test_update_open_order(limit_buy_order):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='BTC_ETH',
|
pair='BTC_ETH',
|
||||||
@ -64,3 +209,103 @@ def test_update_invalid_order(limit_buy_order):
|
|||||||
limit_buy_order['type'] = 'invalid'
|
limit_buy_order['type'] = 'invalid'
|
||||||
with pytest.raises(ValueError, match=r'Unknown order type'):
|
with pytest.raises(ValueError, match=r'Unknown order type'):
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_open_trade_price(limit_buy_order):
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee=0.0025,
|
||||||
|
exchange=Exchanges.BITTREX,
|
||||||
|
)
|
||||||
|
trade.open_order_id = 'open_trade'
|
||||||
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
|
# Get the open rate price with the standard fee rate
|
||||||
|
assert trade.calc_open_trade_price() == 0.001002500
|
||||||
|
|
||||||
|
# Get the open rate price with a custom fee rate
|
||||||
|
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee=0.0025,
|
||||||
|
exchange=Exchanges.BITTREX,
|
||||||
|
)
|
||||||
|
trade.open_order_id = 'close_trade'
|
||||||
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
|
# Get the close rate price with a custom close rate and a regular fee rate
|
||||||
|
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318
|
||||||
|
|
||||||
|
# Get the close rate price with a custom close rate and a custom fee rate
|
||||||
|
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704
|
||||||
|
|
||||||
|
# Test when we apply a Sell order, and ask price with a custom fee rate
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_profit(limit_buy_order, limit_sell_order):
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee=0.0025,
|
||||||
|
exchange=Exchanges.BITTREX,
|
||||||
|
)
|
||||||
|
trade.open_order_id = 'profit_percent'
|
||||||
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
|
# Custom closing rate and regular fee rate
|
||||||
|
# Higher than open rate
|
||||||
|
assert trade.calc_profit(rate=0.00001234) == 0.00011753
|
||||||
|
# Lower than open rate
|
||||||
|
assert trade.calc_profit(rate=0.00000123) == -0.00089086
|
||||||
|
|
||||||
|
# Custom closing rate and custom fee rate
|
||||||
|
# Higher than open rate
|
||||||
|
assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697
|
||||||
|
# Lower than open rate
|
||||||
|
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
|
||||||
|
|
||||||
|
# Only custom fee without sell order applied
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
trade.calc_profit(fee=0.003)
|
||||||
|
|
||||||
|
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
assert trade.calc_profit() == 0.00006217
|
||||||
|
|
||||||
|
# Test with a custom fee rate on the close trade
|
||||||
|
assert trade.calc_profit(fee=0.003) == 0.00006163
|
||||||
|
|
||||||
|
|
||||||
|
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=0.001,
|
||||||
|
fee=0.0025,
|
||||||
|
exchange=Exchanges.BITTREX,
|
||||||
|
)
|
||||||
|
trade.open_order_id = 'profit_percent'
|
||||||
|
trade.update(limit_buy_order) # Buy @ 0.00001099
|
||||||
|
|
||||||
|
# Get percent of profit with a custom rate (Higher than open rate)
|
||||||
|
assert trade.calc_profit_percent(rate=0.00001234) == 0.1172387
|
||||||
|
|
||||||
|
# Get percent of profit with a custom rate (Lower than open rate)
|
||||||
|
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
|
||||||
|
|
||||||
|
# Only custom fee without sell order applied
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
trade.calc_profit_percent(fee=0.003)
|
||||||
|
|
||||||
|
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
|
||||||
|
trade.update(limit_sell_order)
|
||||||
|
assert trade.calc_profit_percent() == 0.06201057
|
||||||
|
|
||||||
|
# Test with a custom fee rate on the close trade
|
||||||
|
assert trade.calc_profit_percent(fee=0.003) == 0.0614782
|
||||||
|
1
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ADA-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ADA-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
2
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
2
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
2
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
2
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
2
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
2
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LTC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LTC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_NXT-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_NXT-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
2
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
2
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_UNITEST-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_UNITEST-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
2
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
File diff suppressed because one or more lines are too long
2
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
2
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XMR-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XMR-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ZEC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ZEC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
8
install_ta-lib.sh
Executable file
8
install_ta-lib.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||||
|
curl -O -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||||
|
tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
|
cd ta-lib && ./configure && make && sudo make install && cd ..
|
||||||
|
else
|
||||||
|
echo "TA-lib already installed, skipping download and build."
|
||||||
|
cd ta-lib && sudo make install && cd ..
|
||||||
|
fi
|
@ -1,12 +1,12 @@
|
|||||||
python-bittrex==0.2.1
|
python-bittrex==0.2.2
|
||||||
SQLAlchemy==1.1.15
|
SQLAlchemy==1.2.0
|
||||||
python-telegram-bot==9.0.0
|
python-telegram-bot==9.0.0
|
||||||
arrow==0.12.0
|
arrow==0.12.0
|
||||||
cachetools==2.0.1
|
cachetools==2.0.1
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.21.0
|
pandas==0.21.1
|
||||||
scikit-learn==0.19.1
|
scikit-learn==0.19.1
|
||||||
scipy==1.0.0
|
scipy==1.0.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
@ -19,6 +19,7 @@ hyperopt==0.1
|
|||||||
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||||
networkx==1.11
|
networkx==1.11
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
|
pymarketcap==3.3.141
|
||||||
|
|
||||||
# Required for plotting data
|
# Required for plotting data
|
||||||
#matplotlib==2.1.0
|
#matplotlib==2.1.0
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
import matplotlib # Install PYQT5 manually if you want to test this helper function
|
import matplotlib # Install PYQT5 manually if you want to test this helper function
|
||||||
matplotlib.use("Qt5Agg")
|
matplotlib.use("Qt5Agg")
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from freqtrade import exchange, analyze
|
from freqtrade import exchange, analyze
|
||||||
|
|
||||||
|
|
||||||
@ -16,7 +15,8 @@ def plot_analyzed_dataframe(pair: str) -> None:
|
|||||||
|
|
||||||
# Init Bittrex to use public API
|
# Init Bittrex to use public API
|
||||||
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
|
||||||
dataframe = analyze.analyze_ticker(pair)
|
ticker = exchange.get_ticker_history(pair)
|
||||||
|
dataframe = analyze.analyze_ticker(ticker)
|
||||||
|
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
|
||||||
@ -51,4 +51,3 @@ def plot_analyzed_dataframe(pair: str) -> None:
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
plot_analyzed_dataframe('BTC_ETH')
|
plot_analyzed_dataframe('BTC_ETH')
|
||||||
|
|
||||||
|
4
setup.cfg
Normal file
4
setup.cfg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[flake8]
|
||||||
|
#ignore =
|
||||||
|
max-line-length = 100
|
||||||
|
max-complexity = 12
|
Loading…
Reference in New Issue
Block a user