Compare commits
116 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
6b15cb9b10 | ||
|
ff4fcdc760 | ||
|
79c3e0583d | ||
|
f6ef8383bb | ||
|
6f5307fda7 | ||
|
37004e331a | ||
|
57acf85b42 | ||
|
96790d50e5 | ||
|
d32ff3410c | ||
|
35838f5e64 | ||
|
913488910c | ||
|
17b984a7cd | ||
|
f79b44eefe | ||
|
146c254c0f | ||
|
ce2966dd7f | ||
|
0fbca8b8ef | ||
|
3f7a583de6 | ||
|
1196983d5f | ||
|
bbb2c7cf62 | ||
|
ff100bdac4 | ||
|
4feb038d0a | ||
|
1792e0fb9b | ||
|
d4f8b3ebbc | ||
|
aeef9bac33 | ||
|
eff361a104 | ||
|
389f9b45bb | ||
|
c9741cb291 | ||
|
bf6f563df2 | ||
|
58f34d4f4b | ||
|
2c4d0144ba | ||
|
afd1a0bf9b | ||
|
37f6c213f6 | ||
|
76736902c6 | ||
|
d266171ed8 | ||
|
e7df373544 | ||
|
aa4b64d0bb | ||
|
4559ddd74f | ||
|
eecc45f8ba | ||
|
d76476040a | ||
|
0c8c149b86 | ||
|
60a7f8614c | ||
|
c31b67bf7a | ||
|
604a888791 | ||
|
bfac1936d9 | ||
|
b1de0de5a5 | ||
|
75ea2c4e1a | ||
|
5e0f143a38 | ||
|
2d983db2e0 | ||
|
d9b01eee15 | ||
![]() |
2e368ef7aa | ||
![]() |
34c774c067 | ||
![]() |
ac32850034 | ||
![]() |
95e5c2e6c1 | ||
|
aef42336e6 | ||
|
f78427d236 | ||
![]() |
b9eb266236 | ||
|
e0896fdd7b | ||
|
11f97ccf87 | ||
|
3506e3ceec | ||
|
27b2624a67 | ||
|
8500032bff | ||
|
b2522b8dbc | ||
|
0f3ceebcd4 | ||
|
f44ab2f44b | ||
|
3fe5302db3 | ||
|
ea62c49c3a | ||
|
02673b94dd | ||
|
17e8bbacc3 | ||
|
463123adc5 | ||
|
5537f0bf5b | ||
|
5551c9ec3b | ||
|
ff145b6306 | ||
|
add6c875d6 | ||
|
378b5a3b14 | ||
|
e14cc2e4f6 | ||
|
616d5b61cc | ||
|
9cca42e371 | ||
|
06ad311aa3 | ||
|
e42edd9de7 | ||
|
3456ead839 | ||
|
a4a1f7961a | ||
|
8057333501 | ||
|
1eee0c91bf | ||
|
53b4c3722e | ||
|
3f6f502e66 | ||
|
d73c656514 | ||
|
f409bdbba8 | ||
|
4b42e1af4b | ||
|
898ab5a370 | ||
|
6389778d49 | ||
|
b85b913657 | ||
|
bbef0edcd1 | ||
|
8d3a6279b2 | ||
|
df4da75535 | ||
|
04bba626a8 | ||
|
9c6c21637d | ||
|
09b27d2094 | ||
|
998a887736 | ||
|
00499fa0d7 | ||
|
0c517ee3b6 | ||
|
b225b0cb90 | ||
|
d045116297 | ||
|
8b859ad358 | ||
|
0085db825d | ||
|
1f1e64560a | ||
|
c9226a329c | ||
|
44cdf3e0c2 | ||
|
b97f0f0705 | ||
|
b2f4778352 | ||
|
272abed807 | ||
|
0437546e4b | ||
![]() |
2df2041d53 | ||
|
3b9d354a62 | ||
|
a45073997d | ||
|
d102a8f8a1 | ||
|
0e5edd08e5 |
2
.coveragerc
Normal file
2
.coveragerc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[run]
|
||||||
|
omit = freqtrade/tests/*
|
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
config.json*
|
||||||
|
*.sqlite
|
@@ -4,10 +4,6 @@ os:
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- 3.6
|
- 3.6
|
||||||
- nightly
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- python: nightly
|
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
@@ -19,9 +15,12 @@ install:
|
|||||||
- tar zxvf ta-lib-0.4.0-src.tar.gz
|
- tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||||
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
|
- 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 -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
script:
|
script:
|
||||||
- python -m unittest
|
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||||
|
after_success:
|
||||||
|
- 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=
|
||||||
|
30
Dockerfile
30
Dockerfile
@@ -1,17 +1,23 @@
|
|||||||
FROM python:3.6.2
|
FROM python:3.6.2
|
||||||
|
|
||||||
RUN pip install numpy
|
# Install TA-lib
|
||||||
RUN apt-get update
|
RUN apt-get update && apt-get -y install build-essential && apt-get clean
|
||||||
RUN apt-get -y install build-essential
|
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
|
||||||
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
tar xzvf - && \
|
||||||
RUN tar zxvf ta-lib-0.4.0-src.tar.gz
|
cd ta-lib && \
|
||||||
RUN cd ta-lib && ./configure && make && make install
|
./configure && make && make install && \
|
||||||
|
cd .. && rm -rf ta-lib
|
||||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||||
|
|
||||||
RUN mkdir -p /freqtrade
|
# Prepare environment
|
||||||
|
RUN mkdir /freqtrade
|
||||||
WORKDIR /freqtrade
|
WORKDIR /freqtrade
|
||||||
|
|
||||||
ADD ./requirements.txt /freqtrade/requirements.txt
|
# Install dependencies
|
||||||
RUN pip install -r requirements.txt
|
COPY requirements.txt /freqtrade/
|
||||||
ADD . /freqtrade
|
RUN pip install -r requirements.txt
|
||||||
CMD python main.py
|
|
||||||
|
# Install and execute
|
||||||
|
COPY . /freqtrade/
|
||||||
|
RUN pip install -e .
|
||||||
|
CMD ["freqtrade"]
|
||||||
|
7
MANIFEST.in
Normal file
7
MANIFEST.in
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
include LICENSE
|
||||||
|
include README.md
|
||||||
|
include config.json.example
|
||||||
|
include freqtrade/exchange/*.py
|
||||||
|
include freqtrade/rpc/*.py
|
||||||
|
include freqtrade/tests/*.py
|
||||||
|
include freqtrade/tests/testdata/*.json
|
73
README.md
73
README.md
@@ -1,6 +1,8 @@
|
|||||||
# freqtrade
|
# freqtrade
|
||||||
|
|
||||||
[](https://travis-ci.org/gcarq/freqtrade)
|
[](https://travis-ci.org/gcarq/freqtrade)
|
||||||
|
[](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
||||||
|
|
||||||
|
|
||||||
Simple High frequency trading bot for crypto currencies.
|
Simple High frequency trading bot for crypto currencies.
|
||||||
Currently supports trading on Bittrex exchange.
|
Currently supports trading on Bittrex exchange.
|
||||||
@@ -28,11 +30,10 @@ in minutes and the value is the minimum ROI in percent.
|
|||||||
See the example below:
|
See the example below:
|
||||||
```
|
```
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"2880": 0.005, # Sell after 48 hours if there is at least 0.5% profit
|
"50": 0.0, # Sell after 30 minutes if the profit is not negative
|
||||||
"1440": 0.01, # Sell after 24 hours if there is at least 1% profit
|
"40": 0.01, # Sell after 25 minutes if there is at least 1% profit
|
||||||
"720": 0.02, # Sell after 12 hours if there is at least 2% profit
|
"30": 0.02, # Sell after 15 minutes if there is at least 2% profit
|
||||||
"360": 0.02, # Sell after 6 hours if there is at least 2% profit
|
"0": 0.045 # Sell immediately if there is at least 4.5% profit
|
||||||
"0": 0.025 # Sell immediately if there is at least 2.5% profit
|
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -45,7 +46,9 @@ Possible values are `running` or `stopped`. (default=`running`)
|
|||||||
If the value is `stopped` the bot has to be started with `/start` first.
|
If the value is `stopped` the bot has to be started with `/start` first.
|
||||||
|
|
||||||
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
|
||||||
use the `last` price and values between those interpolate between ask and last price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary.
|
use the `last` price and values between those interpolate between ask and last
|
||||||
|
price. Using `ask` price will guarantee quick success in bid, but bot will also
|
||||||
|
end up paying more then would probably have been necessary.
|
||||||
|
|
||||||
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.
|
||||||
@@ -56,6 +59,11 @@ if not feel free to raise a github issue.
|
|||||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||||
|
|
||||||
#### Install
|
#### Install
|
||||||
|
|
||||||
|
`master` branch contains the latest stable release.
|
||||||
|
|
||||||
|
`develop` branch has often new features, but might also cause breaking changes. To use it, you are encouraged to join our [slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cd freqtrade/
|
$ cd freqtrade/
|
||||||
# copy example config. Dont forget to insert your api keys
|
# copy example config. Dont forget to insert your api keys
|
||||||
@@ -63,7 +71,8 @@ $ cp config.json.example config.json
|
|||||||
$ python -m venv .env
|
$ python -m venv .env
|
||||||
$ source .env/bin/activate
|
$ source .env/bin/activate
|
||||||
$ pip install -r requirements.txt
|
$ pip install -r requirements.txt
|
||||||
$ ./main.py
|
$ pip install -e .
|
||||||
|
$ ./freqtrade/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).
|
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).
|
||||||
@@ -71,16 +80,62 @@ There is also an [article](https://www.sales4k.com/blockchain/high-frequency-tra
|
|||||||
#### Execute tests
|
#### Execute tests
|
||||||
|
|
||||||
```
|
```
|
||||||
$ python -m unittest
|
$ pytest
|
||||||
|
```
|
||||||
|
This will by default skip the slow running backtest set. To run backtest set:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
|
Building the image:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cd freqtrade
|
$ cd freqtrade
|
||||||
$ docker build -t freqtrade .
|
$ docker build -t freqtrade .
|
||||||
$ docker run --rm -it freqtrade
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For security reasons, your configuration file will not be included in the
|
||||||
|
image, you will need to bind mount it. It is also advised to bind mount
|
||||||
|
a SQLite database file (see second example) to keep it between updates.
|
||||||
|
|
||||||
|
You can run a one-off container that is immediately deleted upon exiting with
|
||||||
|
the following command (config.json must be in the current working directory):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
To run a restartable instance in the background (feel free to place your
|
||||||
|
configuration and database files wherever it feels comfortable on your
|
||||||
|
filesystem):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cd ~/.freq
|
||||||
|
$ touch tradesv2.sqlite
|
||||||
|
$ docker run -d \
|
||||||
|
--name freqtrade \
|
||||||
|
-v ~/.freq/config.json:/freqtrade/config.json \
|
||||||
|
-v ~/.freq/tradesv2.sqlite:/freqtrade/tradesv2.sqlite \
|
||||||
|
freqtrade
|
||||||
|
```
|
||||||
|
If you are using `dry_run=True` you need to bind `tradesv2.dry_run.sqlite` instead of `tradesv2.sqlite`.
|
||||||
|
|
||||||
|
You can then use the following commands to monitor and manage your container:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ docker logs freqtrade
|
||||||
|
$ docker logs -f freqtrade
|
||||||
|
$ docker restart freqtrade
|
||||||
|
$ docker stop freqtrade
|
||||||
|
$ docker start freqtrade
|
||||||
|
```
|
||||||
|
|
||||||
|
You do not need to rebuild the image for configuration
|
||||||
|
changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
#### Contributing
|
#### Contributing
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
||||||
|
4
bin/freqtrade
Executable file
4
bin/freqtrade
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from freqtrade.main import main
|
||||||
|
main()
|
@@ -4,16 +4,17 @@
|
|||||||
"stake_amount": 0.05,
|
"stake_amount": 0.05,
|
||||||
"dry_run": false,
|
"dry_run": false,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"2880": 0.005,
|
"50": 0.0,
|
||||||
"720": 0.01,
|
"40": 0.01,
|
||||||
"0": 0.02
|
"30": 0.02,
|
||||||
|
"0": 0.045
|
||||||
},
|
},
|
||||||
"stoploss": -0.10,
|
"stoploss": -0.40,
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
"bittrex": {
|
"exchange": {
|
||||||
"enabled": true,
|
"name": "bittrex",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
|
170
exchange.py
170
exchange.py
@@ -1,170 +0,0 @@
|
|||||||
import enum
|
|
||||||
import logging
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from bittrex.bittrex import Bittrex
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Current selected exchange
|
|
||||||
EXCHANGE = None
|
|
||||||
_API = None
|
|
||||||
_CONF = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Exchange(enum.Enum):
|
|
||||||
BITTREX = 1
|
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
|
||||||
"""
|
|
||||||
Initializes this module with the given config,
|
|
||||||
it does basic validation whether the specified
|
|
||||||
exchange and pairs are valid.
|
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
global _API, EXCHANGE
|
|
||||||
|
|
||||||
_CONF.update(config)
|
|
||||||
|
|
||||||
if config['dry_run']:
|
|
||||||
logger.info('Instance is running with dry_run enabled')
|
|
||||||
|
|
||||||
use_bittrex = config.get('bittrex', {}).get('enabled', False)
|
|
||||||
if use_bittrex:
|
|
||||||
EXCHANGE = Exchange.BITTREX
|
|
||||||
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
|
||||||
else:
|
|
||||||
raise RuntimeError('No exchange specified. Aborting!')
|
|
||||||
|
|
||||||
# Check if all pairs are available
|
|
||||||
markets = get_markets()
|
|
||||||
exchange_name = EXCHANGE.name.lower()
|
|
||||||
for pair in config[exchange_name]['pair_whitelist']:
|
|
||||||
if pair not in markets:
|
|
||||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name))
|
|
||||||
|
|
||||||
|
|
||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
|
||||||
"""
|
|
||||||
Places a limit buy order.
|
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
|
||||||
:param rate: Rate limit for order
|
|
||||||
:param amount: The amount to purchase
|
|
||||||
:return: order_id of the placed buy order
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return 'dry_run'
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return data['result']['uuid']
|
|
||||||
|
|
||||||
|
|
||||||
def sell(pair: str, rate: float, amount: float) -> str:
|
|
||||||
"""
|
|
||||||
Places a limit sell order.
|
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
|
||||||
:param rate: Rate limit for order
|
|
||||||
:param amount: The amount to sell
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return 'dry_run'
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return data['result']['uuid']
|
|
||||||
|
|
||||||
|
|
||||||
def get_balance(currency: str) -> float:
|
|
||||||
"""
|
|
||||||
Get account balance.
|
|
||||||
:param currency: currency as str, format: BTC
|
|
||||||
:return: float
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return 999.9
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.get_balance(currency)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return float(data['result']['Balance'] or 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ticker(pair: str) -> dict:
|
|
||||||
"""
|
|
||||||
Get Ticker for given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:return: dict
|
|
||||||
"""
|
|
||||||
if EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.get_ticker(pair.replace('_', '-'))
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return {
|
|
||||||
'bid': float(data['result']['Bid']),
|
|
||||||
'ask': float(data['result']['Ask']),
|
|
||||||
'last': float(data['result']['Last']),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_order(order_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Cancel order for given order_id
|
|
||||||
:param order_id: id as str
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
pass
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.cancel(order_id)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
|
|
||||||
|
|
||||||
def get_open_orders(pair: str) -> List[dict]:
|
|
||||||
"""
|
|
||||||
Get all open orders for given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:return: list of dicts
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return []
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.get_open_orders(pair.replace('_', '-'))
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return [{
|
|
||||||
'id': entry['OrderUuid'],
|
|
||||||
'type': entry['OrderType'],
|
|
||||||
'opened': entry['Opened'],
|
|
||||||
'rate': entry['PricePerUnit'],
|
|
||||||
'amount': entry['Quantity'],
|
|
||||||
'remaining': entry['QuantityRemaining'],
|
|
||||||
} for entry in data['result']]
|
|
||||||
|
|
||||||
|
|
||||||
def get_pair_detail_url(pair: str) -> str:
|
|
||||||
"""
|
|
||||||
Returns the market detail url for the given pair
|
|
||||||
:param pair: pair as str, format: BTC_ANT
|
|
||||||
:return: url as str
|
|
||||||
"""
|
|
||||||
if EXCHANGE == Exchange.BITTREX:
|
|
||||||
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
|
||||||
|
|
||||||
|
|
||||||
def get_markets() -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns all available markets
|
|
||||||
:return: list of all available pairs
|
|
||||||
"""
|
|
||||||
if EXCHANGE == Exchange. BITTREX:
|
|
||||||
data = _API.get_markets()
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
|
3
freqtrade/__init__.py
Normal file
3
freqtrade/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
__version__ = '0.12.0'
|
||||||
|
|
||||||
|
from . import main
|
@@ -1,36 +1,19 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
|
||||||
import arrow
|
|
||||||
import requests
|
|
||||||
from pandas import DataFrame
|
|
||||||
import talib.abstract as ta
|
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import exchange
|
||||||
|
from freqtrade.exchange import Bittrex, get_ticker_history
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict:
|
|
||||||
"""
|
|
||||||
Request ticker data from Bittrex for a given currency pair
|
|
||||||
"""
|
|
||||||
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'marketName': pair.replace('_', '-'),
|
|
||||||
'tickInterval': 'fiveMin',
|
|
||||||
'_': minimum_date.timestamp * 1000
|
|
||||||
}
|
|
||||||
data = requests.get(url, params=params, headers=headers).json()
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Analyses the trend for the given pair
|
Analyses the trend for the given pair
|
||||||
@@ -41,17 +24,23 @@ def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame
|
|||||||
.drop('BV', 1) \
|
.drop('BV', 1) \
|
||||||
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
|
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
|
||||||
.sort_values('date')
|
.sort_values('date')
|
||||||
return df[df['date'].map(arrow.get) > minimum_date]
|
return df
|
||||||
|
|
||||||
|
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
"""
|
"""
|
||||||
dataframe['ema'] = ta.EMA(dataframe, timeperiod=33)
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.22)
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
stoch = ta.STOCHF(dataframe)
|
||||||
|
dataframe['fastd'] = stoch['fastd']
|
||||||
|
dataframe['fastk'] = stoch['fastk']
|
||||||
|
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||||
|
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||||
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
|
dataframe['cci'] = ta.CCI(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
@@ -61,26 +50,12 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
prev_sar = dataframe['sar'].shift(1)
|
|
||||||
prev_close = dataframe['close'].shift(1)
|
|
||||||
prev_sar2 = dataframe['sar'].shift(2)
|
|
||||||
prev_close2 = dataframe['close'].shift(2)
|
|
||||||
|
|
||||||
# wait for stable turn from bearish to bullish market
|
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
(dataframe['close'] > dataframe['sar']) &
|
(dataframe['close'] < dataframe['sma']) &
|
||||||
(prev_close > prev_sar) &
|
(dataframe['tema'] <= dataframe['blower']) &
|
||||||
(prev_close2 < prev_sar2),
|
(dataframe['mfi'] < 25) &
|
||||||
'swap'
|
(dataframe['fastd'] < 25) &
|
||||||
] = 1
|
(dataframe['adx'] > 30),
|
||||||
|
|
||||||
# consider prices above ema to be in upswing
|
|
||||||
dataframe.loc[dataframe['ema'] <= dataframe['close'], 'upswing'] = 1
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
(dataframe['upswing'] == 1) &
|
|
||||||
(dataframe['swap'] == 1) &
|
|
||||||
(dataframe['adx'] > 25), # adx over 25 tells there's enough momentum
|
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
|
|
||||||
@@ -93,13 +68,19 @@ def analyze_ticker(pair: str) -> DataFrame:
|
|||||||
add several TA indicators and buy signal to it
|
add several TA indicators and buy signal to it
|
||||||
:return DataFrame with ticker data and indicator data
|
:return DataFrame with ticker data and indicator data
|
||||||
"""
|
"""
|
||||||
minimum_date = arrow.utcnow().shift(hours=-6)
|
minimum_date = arrow.utcnow().shift(hours=-24)
|
||||||
data = get_ticker(pair, minimum_date)
|
data = get_ticker_history(pair, minimum_date)
|
||||||
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
||||||
|
|
||||||
|
if dataframe.empty:
|
||||||
|
logger.warning('Empty dataframe for pair %s', pair)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
dataframe = populate_indicators(dataframe)
|
dataframe = populate_indicators(dataframe)
|
||||||
dataframe = populate_buy_trend(dataframe)
|
dataframe = populate_buy_trend(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def get_buy_signal(pair: str) -> bool:
|
def get_buy_signal(pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Calculates a buy signal based several technical analysis indicators
|
Calculates a buy signal based several technical analysis indicators
|
||||||
@@ -107,6 +88,10 @@ def get_buy_signal(pair: str) -> bool:
|
|||||||
:return: True if pair is good for buying, False otherwise
|
:return: True if pair is good for buying, False otherwise
|
||||||
"""
|
"""
|
||||||
dataframe = analyze_ticker(pair)
|
dataframe = analyze_ticker(pair)
|
||||||
|
|
||||||
|
if dataframe.empty:
|
||||||
|
return False
|
||||||
|
|
||||||
latest = dataframe.iloc[-1]
|
latest = dataframe.iloc[-1]
|
||||||
|
|
||||||
# Check if dataframe is out of date
|
# Check if dataframe is out of date
|
||||||
@@ -133,19 +118,26 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
|||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
# Two subplots sharing x axis
|
# Two subplots sharing x axis
|
||||||
fig, (ax1, ax2) = plt.subplots(2, sharex=True)
|
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||||
ax1.plot(dataframe.index.values, dataframe['sar'], 'g_', label='pSAR')
|
|
||||||
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
|
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
|
||||||
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
|
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
|
||||||
ax1.plot(dataframe.index.values, dataframe['ema'], '--', label='EMA(20)')
|
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
|
||||||
ax1.plot(dataframe.index.values, dataframe['buy'], 'bo', label='buy')
|
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
|
||||||
|
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
|
||||||
|
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
|
||||||
ax1.legend()
|
ax1.legend()
|
||||||
|
|
||||||
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
||||||
ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
|
||||||
|
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
||||||
ax2.legend()
|
ax2.legend()
|
||||||
|
|
||||||
|
ax3.plot(dataframe.index.values, dataframe['fastk'], label='k')
|
||||||
|
ax3.plot(dataframe.index.values, dataframe['fastd'], label='d')
|
||||||
|
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
|
||||||
|
ax3.legend()
|
||||||
|
|
||||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||||
# all but bottom plot.
|
# all but bottom plot.
|
||||||
fig.subplots_adjust(hspace=0)
|
fig.subplots_adjust(hspace=0)
|
||||||
@@ -156,8 +148,9 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Install PYQT5==5.9 manually if you want to test this helper function
|
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||||
while True:
|
while True:
|
||||||
test_pair = 'BTC_ANT'
|
exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''})
|
||||||
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
test_pair = 'BTC_ETH'
|
||||||
# get_buy_signal(pair)
|
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||||
|
# get_buy_signal(pair)
|
||||||
plot_dataframe(analyze_ticker(test_pair), test_pair)
|
plot_dataframe(analyze_ticker(test_pair), test_pair)
|
||||||
time.sleep(60)
|
time.sleep(60)
|
115
freqtrade/exchange/__init__.py
Normal file
115
freqtrade/exchange/__init__.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import enum
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Current selected exchange
|
||||||
|
EXCHANGE: Exchange = None
|
||||||
|
_CONF: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Exchanges(enum.Enum):
|
||||||
|
"""
|
||||||
|
Maps supported exchange names to correspondent classes.
|
||||||
|
"""
|
||||||
|
BITTREX = Bittrex
|
||||||
|
|
||||||
|
|
||||||
|
def init(config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Initializes this module with the given config,
|
||||||
|
it does basic validation whether the specified
|
||||||
|
exchange and pairs are valid.
|
||||||
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
global _CONF, EXCHANGE
|
||||||
|
|
||||||
|
_CONF.update(config)
|
||||||
|
|
||||||
|
if config['dry_run']:
|
||||||
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
|
||||||
|
exchange_config = config['exchange']
|
||||||
|
|
||||||
|
# Find matching class for the given exchange name
|
||||||
|
name = exchange_config['name']
|
||||||
|
try:
|
||||||
|
exchange_class = Exchanges[name.upper()].value
|
||||||
|
except KeyError:
|
||||||
|
raise RuntimeError('Exchange {} is not supported'.format(name))
|
||||||
|
|
||||||
|
EXCHANGE = exchange_class(exchange_config)
|
||||||
|
|
||||||
|
# Check if all pairs are available
|
||||||
|
validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_pairs(pairs: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Checks if all given pairs are tradable on the current exchange.
|
||||||
|
Raises RuntimeError if one pair is not available.
|
||||||
|
:param pairs: list of pairs
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
markets = EXCHANGE.get_markets()
|
||||||
|
for pair in pairs:
|
||||||
|
if pair not in markets:
|
||||||
|
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower()))
|
||||||
|
|
||||||
|
|
||||||
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return 'dry_run'
|
||||||
|
|
||||||
|
return EXCHANGE.buy(pair, rate, amount)
|
||||||
|
|
||||||
|
|
||||||
|
def sell(pair: str, rate: float, amount: float) -> str:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return 'dry_run'
|
||||||
|
|
||||||
|
return EXCHANGE.sell(pair, rate, amount)
|
||||||
|
|
||||||
|
|
||||||
|
def get_balance(currency: str) -> float:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return 999.9
|
||||||
|
|
||||||
|
return EXCHANGE.get_balance(currency)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ticker(pair: str) -> dict:
|
||||||
|
return EXCHANGE.get_ticker(pair)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
|
||||||
|
return EXCHANGE.get_ticker_history(pair, minimum_date)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_order(order_id: str) -> None:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return
|
||||||
|
|
||||||
|
return EXCHANGE.cancel_order(order_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_open_orders(pair: str) -> List[dict]:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return EXCHANGE.get_open_orders(pair)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pair_detail_url(pair: str) -> str:
|
||||||
|
return EXCHANGE.get_pair_detail_url(pair)
|
||||||
|
|
||||||
|
|
||||||
|
def get_markets() -> List[str]:
|
||||||
|
return EXCHANGE.get_markets()
|
109
freqtrade/exchange/bittrex.py
Normal file
109
freqtrade/exchange/bittrex.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import requests
|
||||||
|
from bittrex.bittrex import Bittrex as _Bittrex
|
||||||
|
|
||||||
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_API: _Bittrex = None
|
||||||
|
_EXCHANGE_CONF: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Bittrex(Exchange):
|
||||||
|
"""
|
||||||
|
Bittrex API wrapper.
|
||||||
|
"""
|
||||||
|
# Base URL and API endpoints
|
||||||
|
BASE_URL: str = 'https://www.bittrex.com'
|
||||||
|
TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks'
|
||||||
|
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
|
||||||
|
# Ticker inveral
|
||||||
|
TICKER_INTERVAL: str = 'fiveMin'
|
||||||
|
# Sleep time to avoid rate limits, used in the main loop
|
||||||
|
SLEEP_TIME: float = 25
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sleep_time(self) -> float:
|
||||||
|
return self.SLEEP_TIME
|
||||||
|
|
||||||
|
def __init__(self, config: dict) -> None:
|
||||||
|
global _API, _EXCHANGE_CONF
|
||||||
|
|
||||||
|
_EXCHANGE_CONF.update(config)
|
||||||
|
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
|
||||||
|
|
||||||
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return data['result']['uuid']
|
||||||
|
|
||||||
|
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return data['result']['uuid']
|
||||||
|
|
||||||
|
def get_balance(self, currency: str) -> float:
|
||||||
|
data = _API.get_balance(currency)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return float(data['result']['Balance'] or 0.0)
|
||||||
|
|
||||||
|
def get_ticker(self, pair: str) -> dict:
|
||||||
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return {
|
||||||
|
'bid': float(data['result']['Bid']),
|
||||||
|
'ask': float(data['result']['Ask']),
|
||||||
|
'last': float(data['result']['Last']),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None):
|
||||||
|
url = self.TICKER_METHOD
|
||||||
|
headers = {
|
||||||
|
# TODO: Set as global setting
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
|
||||||
|
}
|
||||||
|
params = {
|
||||||
|
'marketName': pair.replace('_', '-'),
|
||||||
|
'tickInterval': self.TICKER_INTERVAL,
|
||||||
|
# TODO: Timestamp has no effect on API response
|
||||||
|
'_': minimum_date.timestamp * 1000
|
||||||
|
}
|
||||||
|
data = requests.get(url, params=params, headers=headers).json()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def cancel_order(self, order_id: str) -> None:
|
||||||
|
data = _API.cancel(order_id)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
|
||||||
|
def get_open_orders(self, pair: str) -> List[dict]:
|
||||||
|
data = _API.get_open_orders(pair.replace('_', '-'))
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return [{
|
||||||
|
'id': entry['OrderUuid'],
|
||||||
|
'type': entry['OrderType'],
|
||||||
|
'opened': entry['Opened'],
|
||||||
|
'rate': entry['PricePerUnit'],
|
||||||
|
'amount': entry['Quantity'],
|
||||||
|
'remaining': entry['QuantityRemaining'],
|
||||||
|
} for entry in data['result']]
|
||||||
|
|
||||||
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
|
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
|
||||||
|
|
||||||
|
def get_markets(self) -> List[str]:
|
||||||
|
data = _API.get_markets()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
127
freqtrade/exchange/interface.py
Normal file
127
freqtrade/exchange/interface.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
|
||||||
|
class Exchange(ABC):
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""
|
||||||
|
Name of the exchange.
|
||||||
|
:return: str representation of the class name
|
||||||
|
"""
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def sleep_time(self) -> float:
|
||||||
|
"""
|
||||||
|
Sleep time in seconds for the main loop to avoid API rate limits.
|
||||||
|
:return: float
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
"""
|
||||||
|
Places a limit buy order.
|
||||||
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
|
:param rate: Rate limit for order
|
||||||
|
:param amount: The amount to purchase
|
||||||
|
:return: order_id of the placed buy order
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
"""
|
||||||
|
Places a limit sell order.
|
||||||
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
|
:param rate: Rate limit for order
|
||||||
|
:param amount: The amount to sell
|
||||||
|
:return: order_id of the placed sell order
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_balance(self, currency: str) -> float:
|
||||||
|
"""
|
||||||
|
Gets account balance.
|
||||||
|
:param currency: Currency as str, format: BTC
|
||||||
|
:return: float
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_ticker(self, pair: str) -> dict:
|
||||||
|
"""
|
||||||
|
Gets ticker for given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:return: dict, format: {
|
||||||
|
'bid': float,
|
||||||
|
'ask': float,
|
||||||
|
'last': float
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None) -> dict:
|
||||||
|
"""
|
||||||
|
Gets ticker history for given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:param minimum_date: Minimum date (optional)
|
||||||
|
:return: dict, format: {
|
||||||
|
'success': bool,
|
||||||
|
'message': str,
|
||||||
|
'result': [
|
||||||
|
{
|
||||||
|
'O': float, (Open)
|
||||||
|
'H': float, (High)
|
||||||
|
'L': float, (Low)
|
||||||
|
'C': float, (Close)
|
||||||
|
'V': float, (Volume)
|
||||||
|
'T': datetime, (Time)
|
||||||
|
'BV': float, (Base Volume)
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def cancel_order(self, order_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Cancels order for given order_id.
|
||||||
|
:param order_id: ID as str
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_open_orders(self, pair: str) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Gets all open orders for given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:return: List of dicts, format: [
|
||||||
|
{
|
||||||
|
'id': str,
|
||||||
|
'type': str,
|
||||||
|
'opened': datetime,
|
||||||
|
'rate': float,
|
||||||
|
'amount': float,
|
||||||
|
'remaining': int,
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
|
"""
|
||||||
|
Returns the market detail url for the given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:return: URL as str
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_markets(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns all available markets.
|
||||||
|
:return: List of all available pairs
|
||||||
|
"""
|
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@@ -8,22 +9,16 @@ from typing import Dict, Optional
|
|||||||
|
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
|
||||||
import exchange
|
from freqtrade import __version__, exchange, persistence
|
||||||
import persistence
|
from freqtrade.analyze import get_buy_signal
|
||||||
from persistence import Trade
|
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
|
||||||
from analyze import get_buy_signal
|
from freqtrade.persistence import Trade
|
||||||
from misc import CONF_SCHEMA, get_state, State, update_state
|
from freqtrade.rpc import telegram
|
||||||
from rpc import telegram
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
__author__ = "gcarq"
|
|
||||||
__copyright__ = "gcarq 2017"
|
|
||||||
__license__ = "GPLv3"
|
|
||||||
__version__ = "0.10.0"
|
|
||||||
|
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +34,7 @@ def _process() -> None:
|
|||||||
if len(trades) < _CONF['max_open_trades']:
|
if len(trades) < _CONF['max_open_trades']:
|
||||||
try:
|
try:
|
||||||
# Create entity and execute trade
|
# Create entity and execute trade
|
||||||
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE)
|
trade = create_trade(float(_CONF['stake_amount']))
|
||||||
if trade:
|
if trade:
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
else:
|
else:
|
||||||
@@ -94,12 +89,9 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
|
|||||||
# Get available balance
|
# Get available balance
|
||||||
currency = trade.pair.split('_')[1]
|
currency = trade.pair.split('_')[1]
|
||||||
balance = exchange.get_balance(currency)
|
balance = exchange.get_balance(currency)
|
||||||
whitelist = _CONF[trade.exchange.name.lower()]['pair_whitelist']
|
|
||||||
|
|
||||||
profit = trade.exec_sell_order(current_rate, balance)
|
profit = trade.exec_sell_order(current_rate, balance)
|
||||||
whitelist.append(trade.pair)
|
|
||||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||||
trade.exchange.name,
|
trade.exchange,
|
||||||
trade.pair.replace('_', '/'),
|
trade.pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
trade.close_rate,
|
trade.close_rate,
|
||||||
@@ -150,6 +142,7 @@ def handle_trade(trade: Trade) -> None:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
logger.exception('Unable to handle open order')
|
logger.exception('Unable to handle open order')
|
||||||
|
|
||||||
|
|
||||||
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 """
|
||||||
if ticker['ask'] < ticker['last']:
|
if ticker['ask'] < ticker['last']:
|
||||||
@@ -158,15 +151,14 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
|
|||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
|
||||||
def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]:
|
def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:param stake_amount: amount of btc to spend
|
:param stake_amount: amount of btc to spend
|
||||||
:param _exchange: exchange to use
|
|
||||||
"""
|
"""
|
||||||
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
||||||
whitelist = _CONF[_exchange.name.lower()]['pair_whitelist']
|
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -174,11 +166,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
# Remove currently opened and latest pairs from whitelist
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||||
latest_trade = Trade.query.filter(Trade.is_open.is_(False)).order_by(Trade.id.desc()).first()
|
|
||||||
if latest_trade:
|
|
||||||
trades.append(latest_trade)
|
|
||||||
for trade in trades:
|
|
||||||
if trade.pair in whitelist:
|
if trade.pair in whitelist:
|
||||||
whitelist.remove(trade.pair)
|
whitelist.remove(trade.pair)
|
||||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||||
@@ -199,7 +187,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
|
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
||||||
_exchange.name,
|
exchange.EXCHANGE.name.upper(),
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(pair),
|
exchange.get_pair_detail_url(pair),
|
||||||
open_rate
|
open_rate
|
||||||
@@ -211,7 +199,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
open_rate=open_rate,
|
open_rate=open_rate,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
amount=amount,
|
amount=amount,
|
||||||
exchange=_exchange,
|
exchange=exchange.EXCHANGE.name.upper(),
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
is_open=True)
|
is_open=True)
|
||||||
|
|
||||||
@@ -238,7 +226,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
|
|
||||||
def app(config: dict) -> None:
|
def app(config: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Main function which handles the application state
|
Main loop which handles the application state
|
||||||
:param config: config as dict
|
:param config: config as dict
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
@@ -260,7 +248,7 @@ def app(config: dict) -> None:
|
|||||||
elif new_state == State.RUNNING:
|
elif new_state == State.RUNNING:
|
||||||
_process()
|
_process()
|
||||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
# We need to sleep here because otherwise we would run into bittrex rate limit
|
||||||
time.sleep(25)
|
time.sleep(exchange.EXCHANGE.sleep_time)
|
||||||
old_state = new_state
|
old_state = new_state
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
||||||
@@ -269,8 +257,17 @@ def app(config: dict) -> None:
|
|||||||
telegram.send_msg('*Status:* `Trader has stopped`')
|
telegram.send_msg('*Status:* `Trader has stopped`')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def main():
|
||||||
|
"""
|
||||||
|
Loads and validates the config and starts the main loop
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
global _CONF
|
||||||
with open('config.json') as file:
|
with open('config.json') as file:
|
||||||
_CONF = json.load(file)
|
_CONF = json.load(file)
|
||||||
validate(_CONF, CONF_SCHEMA)
|
validate(_CONF, CONF_SCHEMA)
|
||||||
app(_CONF)
|
app(_CONF)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@@ -60,7 +60,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['ask_last_balance']
|
'required': ['ask_last_balance']
|
||||||
},
|
},
|
||||||
'bittrex': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'telegram': {
|
'telegram': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
@@ -76,7 +76,7 @@ CONF_SCHEMA = {
|
|||||||
'exchange': {
|
'exchange': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'enabled': {'type': 'boolean'},
|
'name': {'type': 'string'},
|
||||||
'key': {'type': 'string'},
|
'key': {'type': 'string'},
|
||||||
'secret': {'type': 'string'},
|
'secret': {'type': 'string'},
|
||||||
'pair_whitelist': {
|
'pair_whitelist': {
|
||||||
@@ -85,11 +85,11 @@ CONF_SCHEMA = {
|
|||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'required': ['enabled', 'key', 'secret', 'pair_whitelist']
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'anyOf': [
|
'anyOf': [
|
||||||
{'required': ['bittrex']}
|
{'required': ['exchange']}
|
||||||
],
|
],
|
||||||
'required': [
|
'required': [
|
||||||
'max_open_trades',
|
'max_open_trades',
|
@@ -5,11 +5,9 @@ from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create
|
|||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm.scoping import scoped_session
|
from sqlalchemy.orm.scoping import scoped_session
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
|
|
||||||
from sqlalchemy.types import Enum
|
from sqlalchemy.types import Enum
|
||||||
|
|
||||||
import exchange
|
from freqtrade import exchange
|
||||||
|
|
||||||
|
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
|
|
||||||
@@ -43,7 +41,7 @@ class Trade(Base):
|
|||||||
__tablename__ = 'trades'
|
__tablename__ = 'trades'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
exchange = Column(Enum(exchange.Exchange), nullable=False)
|
exchange = Column(String, nullable=False)
|
||||||
pair = Column(String, nullable=False)
|
pair = Column(String, nullable=False)
|
||||||
is_open = Column(Boolean, nullable=False, default=True)
|
is_open = Column(Boolean, nullable=False, default=True)
|
||||||
open_rate = Column(Float, nullable=False)
|
open_rate = Column(Float, nullable=False)
|
@@ -4,14 +4,13 @@ from typing import Callable, Any
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import and_, func, text
|
from sqlalchemy import and_, func, text
|
||||||
|
from telegram import ParseMode, Bot, Update
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
from telegram import ParseMode, Bot, Update
|
|
||||||
|
|
||||||
from misc import get_state, State, update_state
|
from freqtrade import exchange
|
||||||
from persistence import Trade
|
from freqtrade.misc import get_state, State, update_state
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
import exchange
|
|
||||||
|
|
||||||
# Remove noisy log messages
|
# Remove noisy log messages
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||||
@@ -31,9 +30,12 @@ def init(config: dict) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _updater
|
global _updater
|
||||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
|
||||||
|
|
||||||
_CONF.update(config)
|
_CONF.update(config)
|
||||||
|
if not _CONF['telegram']['enabled']:
|
||||||
|
return
|
||||||
|
|
||||||
|
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||||
|
|
||||||
# Register command handler and start telegram message polling
|
# Register command handler and start telegram message polling
|
||||||
handles = [
|
handles = [
|
||||||
@@ -43,6 +45,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('help', _help),
|
||||||
]
|
]
|
||||||
for handle in handles:
|
for handle in handles:
|
||||||
_updater.dispatcher.add_handler(handle)
|
_updater.dispatcher.add_handler(handle)
|
||||||
@@ -255,7 +258,7 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
|||||||
# Execute sell
|
# Execute sell
|
||||||
profit = trade.exec_sell_order(current_rate, balance)
|
profit = trade.exec_sell_order(current_rate, balance)
|
||||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||||
trade.exchange.name,
|
trade.exchange,
|
||||||
trade.pair.replace('_', '/'),
|
trade.pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
trade.close_rate,
|
trade.close_rate,
|
||||||
@@ -299,6 +302,27 @@ def _performance(bot: Bot, update: Update) -> None:
|
|||||||
send_msg(message, parse_mode=ParseMode.HTML)
|
send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _help(bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /help.
|
||||||
|
Show commands of the bot
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
message = """
|
||||||
|
*/start:* `Starts the trader`
|
||||||
|
*/stop:* `Stops the trader`
|
||||||
|
*/status:* `Lists all open trades`
|
||||||
|
*/profit:* `Lists cumulative profit from all finished trades`
|
||||||
|
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
|
||||||
|
*/performance:* `Show performance of each finished trade grouped by pair`
|
||||||
|
*/help:* `This help message`
|
||||||
|
"""
|
||||||
|
send_msg(message, bot=bot)
|
||||||
|
|
||||||
|
|
||||||
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
|
47
freqtrade/tests/test_analyze.py
Normal file
47
freqtrade/tests/test_analyze.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import pytest
|
||||||
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||||
|
get_buy_signal
|
||||||
|
|
||||||
|
RESULT_BITTREX = {
|
||||||
|
'success': True,
|
||||||
|
'message': '',
|
||||||
|
'result': [
|
||||||
|
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 22.17210568, 'T': '2017-08-30T10:40:00', 'BV': 0.01448082},
|
||||||
|
{'O': 0.00066194, 'H': 0.00066195, 'L': 0.00066194, 'C': 0.00066195, 'V': 33.4727437, 'T': '2017-08-30T10:34:00', 'BV': 0.02215696},
|
||||||
|
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 53.85127609, 'T': '2017-08-30T10:37:00', 'BV': 0.0351708},
|
||||||
|
{'O': 0.00066194, 'H': 0.00066194, 'L': 0.00065311, 'C': 0.00065311, 'V': 46.29210665, 'T': '2017-08-30T10:42:00', 'BV': 0.03063118},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def result():
|
||||||
|
return parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
|
||||||
|
|
||||||
|
def test_dataframe_has_correct_columns(result):
|
||||||
|
assert result.columns.tolist() == \
|
||||||
|
['close', 'high', 'low', 'open', 'date', 'volume']
|
||||||
|
|
||||||
|
def test_orders_by_date(result):
|
||||||
|
assert result['date'].tolist() == \
|
||||||
|
['2017-08-30T10:34:00',
|
||||||
|
'2017-08-30T10:37:00',
|
||||||
|
'2017-08-30T10:40:00',
|
||||||
|
'2017-08-30T10:42:00']
|
||||||
|
|
||||||
|
def test_populates_buy_trend(result):
|
||||||
|
dataframe = populate_buy_trend(populate_indicators(result))
|
||||||
|
assert 'buy' in dataframe.columns
|
||||||
|
assert 'buy_price' in dataframe.columns
|
||||||
|
|
||||||
|
def test_returns_latest_buy_signal(mocker):
|
||||||
|
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
|
assert get_buy_signal('BTC-ETH')
|
||||||
|
|
||||||
|
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
|
assert not get_buy_signal('BTC-ETH')
|
77
freqtrade/tests/test_backtesting.py
Normal file
77
freqtrade/tests/test_backtesting.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.analyze import analyze_ticker
|
||||||
|
from freqtrade.main import should_sell
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||||
|
|
||||||
|
def print_results(results):
|
||||||
|
print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
||||||
|
len(results.index),
|
||||||
|
results.profit.mean() * 100.0,
|
||||||
|
results.profit.sum(),
|
||||||
|
results.duration.mean() * 5
|
||||||
|
))
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pairs():
|
||||||
|
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||||
|
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conf():
|
||||||
|
return {
|
||||||
|
"minimal_roi": {
|
||||||
|
"50": 0.0,
|
||||||
|
"40": 0.01,
|
||||||
|
"30": 0.02,
|
||||||
|
"0": 0.045
|
||||||
|
},
|
||||||
|
"stoploss": -0.40
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
|
def test_backtest(conf, pairs, mocker):
|
||||||
|
trades = []
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
for pair in pairs:
|
||||||
|
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
||||||
|
data = json.load(data_file)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
|
||||||
|
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||||
|
ticker = analyze_ticker(pair)
|
||||||
|
# for each buy point
|
||||||
|
for index, row in ticker[ticker.buy == 1].iterrows():
|
||||||
|
trade = Trade(
|
||||||
|
open_rate=row['close'],
|
||||||
|
open_date=arrow.get(row['date']).datetime,
|
||||||
|
amount=1,
|
||||||
|
)
|
||||||
|
# calculate win/lose forwards from buy point
|
||||||
|
for index2, row2 in ticker[index:].iterrows():
|
||||||
|
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
|
||||||
|
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
|
||||||
|
|
||||||
|
trades.append((pair, current_profit, index2 - index))
|
||||||
|
break
|
||||||
|
|
||||||
|
labels = ['currency', 'profit', 'duration']
|
||||||
|
results = DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
print('====================== BACKTESTING REPORT ================================')
|
||||||
|
|
||||||
|
for pair in pairs:
|
||||||
|
print('For currency {}:'.format(pair))
|
||||||
|
print_results(results[results.currency == pair])
|
||||||
|
print('TOTAL OVER ALL TRADES:')
|
||||||
|
print_results(results)
|
166
freqtrade/tests/test_hyperopt.py
Normal file
166
freqtrade/tests/test_hyperopt.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import arrow
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from hyperopt import fmin, tpe, hp
|
||||||
|
|
||||||
|
from freqtrade.analyze import analyze_ticker
|
||||||
|
from freqtrade.main import should_sell
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||||
|
|
||||||
|
def print_results(results):
|
||||||
|
print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
||||||
|
len(results.index),
|
||||||
|
results.profit.mean() * 100.0,
|
||||||
|
results.profit.sum(),
|
||||||
|
results.duration.mean() * 5
|
||||||
|
))
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def pairs():
|
||||||
|
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||||
|
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conf():
|
||||||
|
return {
|
||||||
|
"minimal_roi": {
|
||||||
|
"40": 0.0,
|
||||||
|
"30": 0.01,
|
||||||
|
"20": 0.02,
|
||||||
|
"0": 0.04
|
||||||
|
},
|
||||||
|
"stoploss": -0.05
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def backtest(conf, pairs, mocker, buy_strategy):
|
||||||
|
trades = []
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
for pair in pairs:
|
||||||
|
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
||||||
|
data = json.load(data_file)
|
||||||
|
|
||||||
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
|
||||||
|
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||||
|
mocker.patch('freqtrade.analyze.populate_buy_trend', side_effect=buy_strategy)
|
||||||
|
ticker = analyze_ticker(pair)
|
||||||
|
# for each buy point
|
||||||
|
for index, row in ticker[ticker.buy == 1].iterrows():
|
||||||
|
trade = Trade(
|
||||||
|
open_rate=row['close'],
|
||||||
|
open_date=arrow.get(row['date']).datetime,
|
||||||
|
amount=1,
|
||||||
|
)
|
||||||
|
# calculate win/lose forwards from buy point
|
||||||
|
for index2, row2 in ticker[index:].iterrows():
|
||||||
|
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
|
||||||
|
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
|
||||||
|
|
||||||
|
trades.append((pair, current_profit, index2 - index))
|
||||||
|
break
|
||||||
|
|
||||||
|
labels = ['currency', 'profit', 'duration']
|
||||||
|
results = DataFrame.from_records(trades, columns=labels)
|
||||||
|
|
||||||
|
print_results(results)
|
||||||
|
|
||||||
|
# set the value below to suit your number concurrent trades so its realistic to 20days of data
|
||||||
|
TARGET_TRADES = 1200
|
||||||
|
if results.profit.sum() == 0 or results.profit.mean() == 0:
|
||||||
|
return 49999999999 # avoid division by zero, return huge value to discard result
|
||||||
|
return abs(len(results.index) - 1200.1) / (results.profit.sum() ** 2) * results.duration.mean() # the smaller the better
|
||||||
|
|
||||||
|
def buy_strategy_generator(params):
|
||||||
|
print(params)
|
||||||
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
|
conditions = []
|
||||||
|
# GUARDS AND TRENDS
|
||||||
|
if params['below_sma']['enabled']:
|
||||||
|
conditions.append(dataframe['close'] < dataframe['sma'])
|
||||||
|
if params['over_sma']['enabled']:
|
||||||
|
conditions.append(dataframe['close'] > dataframe['sma'])
|
||||||
|
if params['mfi']['enabled']:
|
||||||
|
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||||
|
if params['fastd']['enabled']:
|
||||||
|
conditions.append(dataframe['fastd'] < params['fastd']['value'])
|
||||||
|
if params['adx']['enabled']:
|
||||||
|
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||||
|
if params['cci']['enabled']:
|
||||||
|
conditions.append(dataframe['cci'] < params['cci']['value'])
|
||||||
|
if params['over_sar']['enabled']:
|
||||||
|
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||||
|
if params['uptrend_sma']['enabled']:
|
||||||
|
prevsma = dataframe['sma'].shift(1)
|
||||||
|
conditions.append(dataframe['sma'] > prevsma)
|
||||||
|
|
||||||
|
prev_fastd = dataframe['fastd'].shift(1)
|
||||||
|
# TRIGGERS
|
||||||
|
triggers = {
|
||||||
|
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
||||||
|
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10),
|
||||||
|
}
|
||||||
|
conditions.append(triggers.get(params['trigger']['type']))
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
reduce(lambda x, y: x & y, conditions),
|
||||||
|
'buy'] = 1
|
||||||
|
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
return populate_buy_trend
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
|
def test_hyperopt(conf, pairs, mocker):
|
||||||
|
|
||||||
|
def optimizer(params):
|
||||||
|
return backtest(conf, pairs, mocker, buy_strategy_generator(params))
|
||||||
|
|
||||||
|
space = {
|
||||||
|
'mfi': hp.choice('mfi', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.uniform('mfi-value', 2, 40)}
|
||||||
|
]),
|
||||||
|
'fastd': hp.choice('fastd', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.uniform('fastd-value', 2, 40)}
|
||||||
|
]),
|
||||||
|
'adx': hp.choice('adx', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.uniform('adx-value', 2, 40)}
|
||||||
|
]),
|
||||||
|
'cci': hp.choice('cci', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True, 'value': hp.uniform('cci-value', -200, -100)}
|
||||||
|
]),
|
||||||
|
'below_sma': hp.choice('below_sma', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'over_sma': hp.choice('over_sma', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'over_sar': hp.choice('over_sar', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'uptrend_sma': hp.choice('uptrend_sma', [
|
||||||
|
{'enabled': False},
|
||||||
|
{'enabled': True}
|
||||||
|
]),
|
||||||
|
'trigger': hp.choice('trigger', [
|
||||||
|
{'type': 'lower_bb'},
|
||||||
|
{'type': 'faststoch10'}
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
|
||||||
|
print('Best parameters {}'.format(fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40)))
|
126
freqtrade/tests/test_main.py
Normal file
126
freqtrade/tests/test_main.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import copy
|
||||||
|
from unittest.mock import MagicMock, call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from jsonschema import validate
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchanges
|
||||||
|
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
||||||
|
get_target_bid
|
||||||
|
from freqtrade.misc import CONF_SCHEMA
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conf():
|
||||||
|
configuration = {
|
||||||
|
"max_open_trades": 3,
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.05,
|
||||||
|
"dry_run": True,
|
||||||
|
"minimal_roi": {
|
||||||
|
"2880": 0.005,
|
||||||
|
"720": 0.01,
|
||||||
|
"0": 0.02
|
||||||
|
},
|
||||||
|
"bid_strategy": {
|
||||||
|
"ask_last_balance": 0.0
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "bittrex",
|
||||||
|
"enabled": True,
|
||||||
|
"key": "key",
|
||||||
|
"secret": "secret",
|
||||||
|
"pair_whitelist": [
|
||||||
|
"BTC_ETH",
|
||||||
|
"BTC_TKN",
|
||||||
|
"BTC_TRST",
|
||||||
|
"BTC_SWT",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"enabled": True,
|
||||||
|
"token": "token",
|
||||||
|
"chat_id": "chat_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validate(configuration, CONF_SCHEMA)
|
||||||
|
return configuration
|
||||||
|
|
||||||
|
def test_create_trade(conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.07256061,
|
||||||
|
'ask': 0.072661,
|
||||||
|
'last': 0.07256061
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value='mocked_order_id'))
|
||||||
|
# Save state of current whitelist
|
||||||
|
whitelist = copy.deepcopy(conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
init(conf, 'sqlite://')
|
||||||
|
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
|
||||||
|
trade = create_trade(15.0)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
assert trade is not None
|
||||||
|
assert trade.open_rate == 0.072661
|
||||||
|
assert trade.pair == pair
|
||||||
|
assert trade.exchange == Exchanges.BITTREX.name
|
||||||
|
assert trade.amount == 206.43811673387373
|
||||||
|
assert trade.stake_amount == 15.0
|
||||||
|
assert trade.is_open
|
||||||
|
assert trade.open_date is not None
|
||||||
|
assert whitelist == conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
buy_signal.assert_has_calls(
|
||||||
|
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_handle_trade(conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.17256061,
|
||||||
|
'ask': 0.172661,
|
||||||
|
'last': 0.17256061
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value='mocked_order_id'))
|
||||||
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
|
assert trade
|
||||||
|
handle_trade(trade)
|
||||||
|
assert trade.close_rate == 0.17256061
|
||||||
|
assert trade.close_profit == 137.4872490056564
|
||||||
|
assert trade.close_date is not None
|
||||||
|
assert trade.open_order_id == 'dry_run'
|
||||||
|
|
||||||
|
def test_close_trade(conf, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
# Simulate that there is no open order
|
||||||
|
trade.open_order_id = None
|
||||||
|
|
||||||
|
closed = close_trade_if_fulfilled(trade)
|
||||||
|
assert closed
|
||||||
|
assert not trade.is_open
|
||||||
|
|
||||||
|
def test_balance_fully_ask_side(mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
||||||
|
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
||||||
|
|
||||||
|
def test_balance_fully_last_side(mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||||
|
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
||||||
|
|
||||||
|
def test_balance_when_last_bigger_than_ask(mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||||
|
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
20
freqtrade/tests/test_persistence.py
Normal file
20
freqtrade/tests/test_persistence.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
from freqtrade.exchange import Exchanges
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
def test_exec_sell_order(mocker):
|
||||||
|
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
|
||||||
|
trade = Trade(
|
||||||
|
pair='BTC_ETH',
|
||||||
|
stake_amount=1.00,
|
||||||
|
open_rate=0.50,
|
||||||
|
amount=10.00,
|
||||||
|
exchange=Exchanges.BITTREX,
|
||||||
|
open_order_id='mocked'
|
||||||
|
)
|
||||||
|
profit = trade.exec_sell_order(1.00, 10.00)
|
||||||
|
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
|
||||||
|
assert profit == 100.0
|
||||||
|
assert trade.close_rate == 1.0
|
||||||
|
assert trade.close_profit == profit
|
||||||
|
assert trade.close_date is not None
|
199
freqtrade/tests/test_telegram.py
Normal file
199
freqtrade/tests/test_telegram.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
from datetime import datetime
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from jsonschema import validate
|
||||||
|
from telegram import Bot, Update, Message, Chat
|
||||||
|
|
||||||
|
from freqtrade.main import init, create_trade
|
||||||
|
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def conf():
|
||||||
|
configuration = {
|
||||||
|
"max_open_trades": 3,
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.05,
|
||||||
|
"dry_run": True,
|
||||||
|
"minimal_roi": {
|
||||||
|
"2880": 0.005,
|
||||||
|
"720": 0.01,
|
||||||
|
"0": 0.02
|
||||||
|
},
|
||||||
|
"bid_strategy": {
|
||||||
|
"ask_last_balance": 0.0
|
||||||
|
},
|
||||||
|
"exchange": {
|
||||||
|
"name": "bittrex",
|
||||||
|
"enabled": True,
|
||||||
|
"key": "key",
|
||||||
|
"secret": "secret",
|
||||||
|
"pair_whitelist": [
|
||||||
|
"BTC_ETH"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"enabled": True,
|
||||||
|
"token": "token",
|
||||||
|
"chat_id": "0"
|
||||||
|
},
|
||||||
|
"initial_state": "running"
|
||||||
|
}
|
||||||
|
validate(configuration, CONF_SCHEMA)
|
||||||
|
return configuration
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def update():
|
||||||
|
_update = Update(0)
|
||||||
|
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
|
||||||
|
return _update
|
||||||
|
|
||||||
|
|
||||||
|
class MagicBot(MagicMock, Bot):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_handle(conf, update, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.07256061,
|
||||||
|
'ask': 0.072661,
|
||||||
|
'last': 0.07256061
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value='mocked_order_id'))
|
||||||
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
trade = create_trade(15.0)
|
||||||
|
assert trade
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
_status(bot=MagicBot(), update=update)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
def test_profit_handle(conf, update, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.07256061,
|
||||||
|
'ask': 0.072661,
|
||||||
|
'last': 0.07256061
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value='mocked_order_id'))
|
||||||
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
trade = create_trade(15.0)
|
||||||
|
assert trade
|
||||||
|
trade.close_rate = 0.07256061
|
||||||
|
trade.close_profit = 100.00
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.is_open = False
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
_profit(bot=MagicBot(), update=update)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
def test_forcesell_handle(conf, update, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.07256061,
|
||||||
|
'ask': 0.072661,
|
||||||
|
'last': 0.07256061
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value='mocked_order_id'))
|
||||||
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
trade = create_trade(15.0)
|
||||||
|
assert trade
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
update.message.text = '/forcesell 1'
|
||||||
|
_forcesell(bot=MagicBot(), update=update)
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert '0.072561' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
def test_performance_handle(conf, update, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
validate_pairs=MagicMock(),
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.07256061,
|
||||||
|
'ask': 0.072661,
|
||||||
|
'last': 0.07256061
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value='mocked_order_id'))
|
||||||
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
trade = create_trade(15.0)
|
||||||
|
assert trade
|
||||||
|
trade.close_rate = 0.07256061
|
||||||
|
trade.close_profit = 100.00
|
||||||
|
trade.close_date = datetime.utcnow()
|
||||||
|
trade.open_order_id = None
|
||||||
|
trade.is_open = False
|
||||||
|
Trade.session.add(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
_performance(bot=MagicBot(), update=update)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
def test_start_handle(conf, update, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
||||||
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
|
update_state(State.STOPPED)
|
||||||
|
assert get_state() == State.STOPPED
|
||||||
|
_start(bot=MagicBot(), update=update)
|
||||||
|
assert get_state() == State.RUNNING
|
||||||
|
assert msg_mock.call_count == 0
|
||||||
|
|
||||||
|
def test_stop_handle(conf, update, mocker):
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
||||||
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
|
update_state(State.RUNNING)
|
||||||
|
assert get_state() == State.RUNNING
|
||||||
|
_stop(bot=MagicBot(), update=update)
|
||||||
|
assert get_state() == State.STOPPED
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
16
freqtrade/tests/testdata/download_backtest_data.py
vendored
Normal file
16
freqtrade/tests/testdata/download_backtest_data.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""This script generate json data from bittrex"""
|
||||||
|
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
CURRENCIES = ["ok", "neo", "dash", "etc", "eth", "snt"]
|
||||||
|
|
||||||
|
for cur in CURRENCIES:
|
||||||
|
url1 = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=BTC-'
|
||||||
|
url = url1+cur+'&tickInterval=fiveMin'
|
||||||
|
x = urlopen(url)
|
||||||
|
json_data = x.read()
|
||||||
|
json_str = str(json_data, 'utf-8')
|
||||||
|
with open('btc-'+cur+'.json', 'w') as file:
|
||||||
|
file.write(json_str)
|
@@ -1,14 +1,23 @@
|
|||||||
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
|
-e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex
|
||||||
SQLAlchemy==1.1.13
|
SQLAlchemy==1.1.14
|
||||||
python-telegram-bot==7.0.1
|
python-telegram-bot==8.1.1
|
||||||
arrow==0.10.0
|
arrow==0.10.0
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.20.3
|
pandas==0.20.3
|
||||||
matplotlib==2.0.2
|
|
||||||
scikit-learn==0.19.0
|
scikit-learn==0.19.0
|
||||||
scipy==0.19.1
|
scipy==0.19.1
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
|
numpy==1.13.3
|
||||||
TA-Lib==0.4.10
|
TA-Lib==0.4.10
|
||||||
|
pytest==3.2.3
|
||||||
|
pytest-mock==1.6.3
|
||||||
|
pytest-cov==2.5.1
|
||||||
|
hyperopt==0.1
|
||||||
|
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
|
||||||
|
networkx==1.11
|
||||||
|
|
||||||
|
# Required for plotting data
|
||||||
|
#matplotlib==2.1.0
|
||||||
#PYQT5==5.9
|
#PYQT5==5.9
|
41
setup.py
Normal file
41
setup.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
from freqtrade import __version__
|
||||||
|
|
||||||
|
|
||||||
|
setup(name='freqtrade',
|
||||||
|
version=__version__,
|
||||||
|
description='Simple High Frequency Trading Bot for crypto currencies',
|
||||||
|
url='https://github.com/gcarq/freqtrade',
|
||||||
|
author='gcarq and contributors',
|
||||||
|
author_email='michael.egger@tsn.at',
|
||||||
|
license='GPLv3',
|
||||||
|
packages=['freqtrade'],
|
||||||
|
scripts=['bin/freqtrade'],
|
||||||
|
setup_requires=['pytest-runner'],
|
||||||
|
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
||||||
|
install_requires=[
|
||||||
|
'python-bittrex==0.1.3',
|
||||||
|
'SQLAlchemy==1.1.13',
|
||||||
|
'python-telegram-bot==8.1.1',
|
||||||
|
'arrow==0.10.0',
|
||||||
|
'requests==2.18.4',
|
||||||
|
'urllib3==1.22',
|
||||||
|
'wrapt==1.10.11',
|
||||||
|
'pandas==0.20.3',
|
||||||
|
'scikit-learn==0.19.0',
|
||||||
|
'scipy==0.19.1',
|
||||||
|
'jsonschema==2.6.0',
|
||||||
|
'TA-Lib==0.4.10',
|
||||||
|
],
|
||||||
|
dependency_links=[
|
||||||
|
"git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex-0.1.3"
|
||||||
|
],
|
||||||
|
include_package_data=True,
|
||||||
|
zip_safe=False,
|
||||||
|
classifiers=[
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||||
|
'Topic :: Office/Business :: Financial :: Investment',
|
||||||
|
'Intended Audience :: Science/Research',
|
||||||
|
])
|
@@ -1,49 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
|
||||||
import unittest
|
|
||||||
from unittest.mock import patch
|
|
||||||
from pandas import DataFrame
|
|
||||||
import arrow
|
|
||||||
from analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, analyze_ticker, get_buy_signal
|
|
||||||
|
|
||||||
RESULT_BITTREX = {
|
|
||||||
'success': True,
|
|
||||||
'message': '',
|
|
||||||
'result': [
|
|
||||||
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 22.17210568, 'T': '2017-08-30T10:40:00', 'BV': 0.01448082},
|
|
||||||
{'O': 0.00066194, 'H': 0.00066195, 'L': 0.00066194, 'C': 0.00066195, 'V': 33.4727437, 'T': '2017-08-30T10:34:00', 'BV': 0.02215696},
|
|
||||||
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 53.85127609, 'T': '2017-08-30T10:37:00', 'BV': 0.0351708},
|
|
||||||
{'O': 0.00066194, 'H': 0.00066194, 'L': 0.00065311, 'C': 0.00065311, 'V': 46.29210665, 'T': '2017-08-30T10:42:00', 'BV': 0.03063118},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestAnalyze(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.result = parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
|
|
||||||
|
|
||||||
def test_1_dataframe_has_correct_columns(self):
|
|
||||||
self.assertEqual(self.result.columns.tolist(),
|
|
||||||
['close', 'high', 'low', 'open', 'date', 'volume'])
|
|
||||||
|
|
||||||
def test_2_orders_by_date(self):
|
|
||||||
self.assertEqual(self.result['date'].tolist(),
|
|
||||||
['2017-08-30T10:34:00',
|
|
||||||
'2017-08-30T10:37:00',
|
|
||||||
'2017-08-30T10:40:00',
|
|
||||||
'2017-08-30T10:42:00'])
|
|
||||||
|
|
||||||
def test_3_populates_buy_trend(self):
|
|
||||||
dataframe = populate_buy_trend(populate_indicators(self.result))
|
|
||||||
self.assertTrue('buy' in dataframe.columns)
|
|
||||||
self.assertTrue('buy_price' in dataframe.columns)
|
|
||||||
|
|
||||||
def test_4_returns_latest_buy_signal(self):
|
|
||||||
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
|
||||||
with patch('analyze.analyze_ticker', return_value=buydf):
|
|
||||||
self.assertEqual(get_buy_signal('BTC-ETH'), True)
|
|
||||||
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
|
||||||
with patch('analyze.analyze_ticker', return_value=buydf):
|
|
||||||
self.assertEqual(get_buy_signal('BTC-ETH'), False)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
@@ -1,71 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
|
||||||
import unittest
|
|
||||||
from unittest.mock import patch
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import arrow
|
|
||||||
from pandas import DataFrame
|
|
||||||
from analyze import analyze_ticker
|
|
||||||
from persistence import Trade
|
|
||||||
from main import should_sell
|
|
||||||
|
|
||||||
def print_results(results):
|
|
||||||
print('Made {} buys. Average profit {:.1f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
|
||||||
len(results.index),
|
|
||||||
results.profit.mean() * 100.0,
|
|
||||||
results.profit.sum(),
|
|
||||||
results.duration.mean()*5
|
|
||||||
))
|
|
||||||
|
|
||||||
class TestMain(unittest.TestCase):
|
|
||||||
pairs = ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
|
||||||
conf = {
|
|
||||||
"minimal_roi": {
|
|
||||||
"2880": 0.005,
|
|
||||||
"720": 0.01,
|
|
||||||
"0": 0.02
|
|
||||||
},
|
|
||||||
"stoploss": -0.10
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
|
||||||
|
|
||||||
@unittest.skipIf(not os.environ.get('BACKTEST', False), "slow, should be run manually")
|
|
||||||
def test_backtest(self):
|
|
||||||
trades = []
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
for pair in self.pairs:
|
|
||||||
with open('test/testdata/'+pair+'.json') as data_file:
|
|
||||||
data = json.load(data_file)
|
|
||||||
|
|
||||||
with patch('analyze.get_ticker', return_value=data):
|
|
||||||
with patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')):
|
|
||||||
ticker = analyze_ticker(pair)
|
|
||||||
# for each buy point
|
|
||||||
for index, row in ticker[ticker.buy == 1].iterrows():
|
|
||||||
trade = Trade(
|
|
||||||
open_rate=row['close'],
|
|
||||||
open_date=arrow.get(row['date']).datetime,
|
|
||||||
amount=1,
|
|
||||||
)
|
|
||||||
# calculate win/lose forwards from buy point
|
|
||||||
for index2, row2 in ticker[index:].iterrows():
|
|
||||||
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
|
|
||||||
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
|
|
||||||
|
|
||||||
trades.append((pair, current_profit, index2 - index))
|
|
||||||
break
|
|
||||||
|
|
||||||
labels = ['currency', 'profit', 'duration']
|
|
||||||
results = DataFrame.from_records(trades, columns=labels)
|
|
||||||
|
|
||||||
print('====================== BACKTESTING REPORT ================================')
|
|
||||||
|
|
||||||
for pair in self.pairs:
|
|
||||||
print('For currency {}:'.format(pair))
|
|
||||||
print_results(results[results.currency == pair])
|
|
||||||
print('TOTAL OVER ALL TRADES:')
|
|
||||||
print_results(results)
|
|
@@ -1,114 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
|
|
||||||
from jsonschema import validate
|
|
||||||
|
|
||||||
import exchange
|
|
||||||
from main import create_trade, handle_trade, close_trade_if_fulfilled, init, get_target_bid
|
|
||||||
from misc import CONF_SCHEMA
|
|
||||||
from persistence import Trade
|
|
||||||
|
|
||||||
|
|
||||||
class TestMain(unittest.TestCase):
|
|
||||||
conf = {
|
|
||||||
"max_open_trades": 3,
|
|
||||||
"stake_currency": "BTC",
|
|
||||||
"stake_amount": 0.05,
|
|
||||||
"dry_run": True,
|
|
||||||
"minimal_roi": {
|
|
||||||
"2880": 0.005,
|
|
||||||
"720": 0.01,
|
|
||||||
"0": 0.02
|
|
||||||
},
|
|
||||||
"bid_strategy": {
|
|
||||||
"ask_last_balance": 0.0
|
|
||||||
},
|
|
||||||
"bittrex": {
|
|
||||||
"enabled": True,
|
|
||||||
"key": "key",
|
|
||||||
"secret": "secret",
|
|
||||||
"pair_whitelist": [
|
|
||||||
"BTC_ETH"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"telegram": {
|
|
||||||
"enabled": True,
|
|
||||||
"token": "token",
|
|
||||||
"chat_id": "chat_id"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_1_create_trade(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True) as buy_signal:
|
|
||||||
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
|
||||||
with patch.multiple('main.exchange',
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.07256061,
|
|
||||||
'ask': 0.072661,
|
|
||||||
'last': 0.07256061
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_order_id')):
|
|
||||||
init(self.conf, 'sqlite://')
|
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
self.assertIsNotNone(trade)
|
|
||||||
self.assertEqual(trade.open_rate, 0.072661)
|
|
||||||
self.assertEqual(trade.pair, 'BTC_ETH')
|
|
||||||
self.assertEqual(trade.exchange, exchange.Exchange.BITTREX)
|
|
||||||
self.assertEqual(trade.amount, 206.43811673387373)
|
|
||||||
self.assertEqual(trade.stake_amount, 15.0)
|
|
||||||
self.assertEqual(trade.is_open, True)
|
|
||||||
self.assertIsNotNone(trade.open_date)
|
|
||||||
buy_signal.assert_called_once_with('BTC_ETH')
|
|
||||||
|
|
||||||
def test_2_handle_trade(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
|
||||||
with patch.multiple('main.exchange',
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.17256061,
|
|
||||||
'ask': 0.172661,
|
|
||||||
'last': 0.17256061
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_order_id')):
|
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
|
||||||
self.assertTrue(trade)
|
|
||||||
handle_trade(trade)
|
|
||||||
self.assertEqual(trade.close_rate, 0.17256061)
|
|
||||||
self.assertEqual(trade.close_profit, 137.4872490056564)
|
|
||||||
self.assertIsNotNone(trade.close_date)
|
|
||||||
self.assertEqual(trade.open_order_id, 'dry_run')
|
|
||||||
|
|
||||||
def test_3_close_trade(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
|
||||||
self.assertTrue(trade)
|
|
||||||
|
|
||||||
# Simulate that there is no open order
|
|
||||||
trade.open_order_id = None
|
|
||||||
|
|
||||||
closed = close_trade_if_fulfilled(trade)
|
|
||||||
self.assertTrue(closed)
|
|
||||||
self.assertEqual(trade.is_open, False)
|
|
||||||
|
|
||||||
def test_balance_fully_ask_side(self):
|
|
||||||
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}):
|
|
||||||
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 20)
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(self):
|
|
||||||
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
|
|
||||||
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 10)
|
|
||||||
|
|
||||||
def test_balance_when_last_bigger_than_ask(self):
|
|
||||||
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
|
|
||||||
self.assertEqual(get_target_bid({'ask': 5, 'last': 10}), 5)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
validate(cls.conf, CONF_SCHEMA)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
@@ -1,28 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from exchange import Exchange
|
|
||||||
from persistence import Trade
|
|
||||||
|
|
||||||
|
|
||||||
class TestTrade(unittest.TestCase):
|
|
||||||
def test_1_exec_sell_order(self):
|
|
||||||
with patch('main.exchange.sell', side_effect='mocked_order_id') as api_mock:
|
|
||||||
trade = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
stake_amount=1.00,
|
|
||||||
open_rate=0.50,
|
|
||||||
amount=10.00,
|
|
||||||
exchange=Exchange.BITTREX,
|
|
||||||
open_order_id='mocked'
|
|
||||||
)
|
|
||||||
profit = trade.exec_sell_order(1.00, 10.00)
|
|
||||||
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
|
|
||||||
self.assertEqual(profit, 100.0)
|
|
||||||
self.assertEqual(trade.close_rate, 1.0)
|
|
||||||
self.assertEqual(trade.close_profit, profit)
|
|
||||||
self.assertIsNotNone(trade.close_date)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
@@ -1,195 +0,0 @@
|
|||||||
import unittest
|
|
||||||
from unittest.mock import patch, MagicMock
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from jsonschema import validate
|
|
||||||
from telegram import Bot, Update, Message, Chat
|
|
||||||
|
|
||||||
import exchange
|
|
||||||
from main import init, create_trade
|
|
||||||
from misc import CONF_SCHEMA, update_state, State, get_state
|
|
||||||
from persistence import Trade
|
|
||||||
from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
|
|
||||||
|
|
||||||
|
|
||||||
class MagicBot(MagicMock, Bot):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestTelegram(unittest.TestCase):
|
|
||||||
|
|
||||||
conf = {
|
|
||||||
"max_open_trades": 3,
|
|
||||||
"stake_currency": "BTC",
|
|
||||||
"stake_amount": 0.05,
|
|
||||||
"dry_run": True,
|
|
||||||
"minimal_roi": {
|
|
||||||
"2880": 0.005,
|
|
||||||
"720": 0.01,
|
|
||||||
"0": 0.02
|
|
||||||
},
|
|
||||||
"bid_strategy": {
|
|
||||||
"ask_last_balance": 0.0
|
|
||||||
},
|
|
||||||
"bittrex": {
|
|
||||||
"enabled": True,
|
|
||||||
"key": "key",
|
|
||||||
"secret": "secret",
|
|
||||||
"pair_whitelist": [
|
|
||||||
"BTC_ETH"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"telegram": {
|
|
||||||
"enabled": True,
|
|
||||||
"token": "token",
|
|
||||||
"chat_id": "0"
|
|
||||||
},
|
|
||||||
"initial_state": "running"
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_1_status_handle(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
|
||||||
with patch.multiple('main.exchange',
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.07256061,
|
|
||||||
'ask': 0.072661,
|
|
||||||
'last': 0.07256061
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_order_id')):
|
|
||||||
init(self.conf, 'sqlite://')
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
|
||||||
self.assertTrue(trade)
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
_status(bot=MagicBot(), update=self.update)
|
|
||||||
self.assertEqual(msg_mock.call_count, 2)
|
|
||||||
self.assertIn('[BTC_ETH]', msg_mock.call_args_list[-1][0][0])
|
|
||||||
|
|
||||||
def test_2_profit_handle(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
|
||||||
with patch.multiple('main.exchange',
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.07256061,
|
|
||||||
'ask': 0.072661,
|
|
||||||
'last': 0.07256061
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_order_id')):
|
|
||||||
init(self.conf, 'sqlite://')
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
|
||||||
self.assertTrue(trade)
|
|
||||||
trade.close_rate = 0.07256061
|
|
||||||
trade.close_profit = 100.00
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.open_order_id = None
|
|
||||||
trade.is_open = False
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
_profit(bot=MagicBot(), update=self.update)
|
|
||||||
self.assertEqual(msg_mock.call_count, 2)
|
|
||||||
self.assertIn('(100.00%)', msg_mock.call_args_list[-1][0][0])
|
|
||||||
|
|
||||||
def test_3_forcesell_handle(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
|
||||||
with patch.multiple('main.exchange',
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.07256061,
|
|
||||||
'ask': 0.072661,
|
|
||||||
'last': 0.07256061
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_order_id')):
|
|
||||||
init(self.conf, 'sqlite://')
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
|
||||||
self.assertTrue(trade)
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
self.update.message.text = '/forcesell 1'
|
|
||||||
_forcesell(bot=MagicBot(), update=self.update)
|
|
||||||
|
|
||||||
self.assertEqual(msg_mock.call_count, 2)
|
|
||||||
self.assertIn('Selling [BTC/ETH]', msg_mock.call_args_list[-1][0][0])
|
|
||||||
self.assertIn('0.072561', msg_mock.call_args_list[-1][0][0])
|
|
||||||
|
|
||||||
def test_4_performance_handle(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
|
||||||
with patch.multiple('main.exchange',
|
|
||||||
get_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.07256061,
|
|
||||||
'ask': 0.072661,
|
|
||||||
'last': 0.07256061
|
|
||||||
}),
|
|
||||||
buy=MagicMock(return_value='mocked_order_id')):
|
|
||||||
init(self.conf, 'sqlite://')
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
|
||||||
self.assertTrue(trade)
|
|
||||||
trade.close_rate = 0.07256061
|
|
||||||
trade.close_profit = 100.00
|
|
||||||
trade.close_date = datetime.utcnow()
|
|
||||||
trade.open_order_id = None
|
|
||||||
trade.is_open = False
|
|
||||||
Trade.session.add(trade)
|
|
||||||
Trade.session.flush()
|
|
||||||
|
|
||||||
_performance(bot=MagicBot(), update=self.update)
|
|
||||||
self.assertEqual(msg_mock.call_count, 2)
|
|
||||||
self.assertIn('Performance', msg_mock.call_args_list[-1][0][0])
|
|
||||||
self.assertIn('BTC_ETH 100.00%', msg_mock.call_args_list[-1][0][0])
|
|
||||||
|
|
||||||
def test_5_start_handle(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
|
||||||
init(self.conf, 'sqlite://')
|
|
||||||
|
|
||||||
update_state(State.STOPPED)
|
|
||||||
self.assertEqual(get_state(), State.STOPPED)
|
|
||||||
_start(bot=MagicBot(), update=self.update)
|
|
||||||
self.assertEqual(get_state(), State.RUNNING)
|
|
||||||
self.assertEqual(msg_mock.call_count, 0)
|
|
||||||
|
|
||||||
def test_6_stop_handle(self):
|
|
||||||
with patch.dict('main._CONF', self.conf):
|
|
||||||
msg_mock = MagicMock()
|
|
||||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
|
||||||
init(self.conf, 'sqlite://')
|
|
||||||
|
|
||||||
update_state(State.RUNNING)
|
|
||||||
self.assertEqual(get_state(), State.RUNNING)
|
|
||||||
_stop(bot=MagicBot(), update=self.update)
|
|
||||||
self.assertEqual(get_state(), State.STOPPED)
|
|
||||||
self.assertEqual(msg_mock.call_count, 1)
|
|
||||||
self.assertIn('Stopping trader', msg_mock.call_args_list[0][0][0])
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.update = Update(0)
|
|
||||||
self.update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
validate(cls.conf, CONF_SCHEMA)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
Reference in New Issue
Block a user