Compare commits
244 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
349a91bd92 | ||
|
991b43b7e5 | ||
|
a0fa6abcdc | ||
|
86501b43c0 | ||
|
80592970e9 | ||
|
567ed4ecda | ||
|
fafbb0abfe | ||
|
0f1a36b8e9 | ||
|
31c03cdce1 | ||
|
a1b91ad1ea | ||
|
6ce6018bb7 | ||
|
18eec0f4d4 | ||
|
32327c45c2 | ||
|
ba485fe2b2 | ||
|
f8084b117e | ||
|
abdddd5193 | ||
|
8eeb02e592 | ||
|
8555271102 | ||
|
d921bae75e | ||
|
a1388ef296 | ||
|
ddc7c94a1d | ||
|
e36444df27 | ||
|
0395c92260 | ||
|
f03395b90d | ||
|
20d5628786 | ||
|
57e089efd3 | ||
|
fbbde9de25 | ||
|
3d42b9fd75 | ||
|
adfae9e75c | ||
|
117dfbb563 | ||
|
e66dc8b027 | ||
|
ae0b49f532 | ||
|
a37ea13fd1 | ||
|
cc29126d61 | ||
|
810f2f9243 | ||
|
60e651cb4c | ||
|
472ce8566d | ||
|
27ac15f298 | ||
|
d12dba16db | ||
|
0f1d114c03 | ||
|
3e7700e9ac | ||
|
60615c232c | ||
|
3884cfb809 | ||
|
caa6e22e53 | ||
|
19f6ff330c | ||
|
8fdd127f72 | ||
|
0a5eba64e2 | ||
|
b82c4444b2 | ||
|
95a17b8f98 | ||
|
325f72fd91 | ||
|
a237225683 | ||
|
29b173f4e7 | ||
|
50a979161c | ||
|
264d71e29e | ||
|
a873688a44 | ||
|
7cc8533b8e | ||
|
04342acff1 | ||
|
c37df0e70d | ||
|
460dfa1031 | ||
|
08a1d3ca1d | ||
|
1daeed4a52 | ||
|
99724e2458 | ||
|
cd18629433 | ||
|
41510fdb32 | ||
|
9cb249610a | ||
|
543857ddb2 | ||
|
1e5b0e8726 | ||
|
0d0d822904 | ||
|
9ff4a7b205 | ||
|
0e96197a94 | ||
|
9b9d0250f7 | ||
|
4a35676794 | ||
|
465c91b9a9 | ||
|
60249af04c | ||
|
c3653dc417 | ||
|
3d61095ba4 | ||
|
7a0be94cde | ||
|
fad6427078 | ||
|
4dfde7f9a2 | ||
|
e2eceaa904 | ||
|
f34af0ad67 | ||
|
e07904d436 | ||
|
26468bef83 | ||
|
ea1b1e11ea | ||
|
e68e6c0a1a | ||
|
7190226c84 | ||
|
6f2915e25e | ||
|
6f7ac0720b | ||
|
b76554a487 | ||
|
8da55c3742 | ||
|
05111edd04 | ||
|
361bdd20d3 | ||
|
8bdace68f6 | ||
|
0e1eb20781 | ||
|
4c2dea83c5 | ||
|
d066817d0b | ||
|
a632121368 | ||
|
473d09b5cd | ||
|
893738d6f0 | ||
|
22cfef7d36 | ||
|
e1bbe1d9a9 | ||
|
ec981b415a | ||
|
57a17697a0 | ||
|
f4fe09ffbf | ||
|
871b5e17ee | ||
|
9b00fc3474 | ||
|
3b1dc36d8a | ||
|
4edf8f2079 | ||
|
54987fd9ca | ||
|
da9c3e7d7d | ||
|
a948142ef5 | ||
|
4f6c3f94e0 | ||
|
25d6d6bbe5 | ||
|
649781d823 | ||
|
08ca7a8166 | ||
|
dd78c62c3d | ||
|
29de1645fe | ||
|
4139b0b0c7 | ||
|
0c33e917d5 | ||
|
e401a016f5 | ||
|
e0fde8665c | ||
|
752520c823 | ||
|
6ba2492360 | ||
|
d5d798f6fa | ||
|
9c9cf76a0d | ||
|
041e201713 | ||
|
e09505b22d | ||
|
6b15cb9b10 | ||
|
ff4fcdc760 | ||
|
f43ba44b15 | ||
|
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 |
4
.coveragerc
Normal file
4
.coveragerc
Normal file
@@ -0,0 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
freqtrade/tests/*
|
||||
freqtrade/vendor/*
|
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
config.json*
|
||||
*.sqlite
|
12
.travis.yml
12
.travis.yml
@@ -4,10 +4,9 @@ os:
|
||||
language: python
|
||||
python:
|
||||
- 3.6
|
||||
- nightly
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: nightly
|
||||
env:
|
||||
- BACKTEST=
|
||||
- BACKTEST=true
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
@@ -19,9 +18,12 @@ install:
|
||||
- tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
|
||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||
- pip install coveralls
|
||||
- pip install -r requirements.txt
|
||||
script:
|
||||
- python -m unittest
|
||||
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
||||
after_success:
|
||||
- coveralls
|
||||
notifications:
|
||||
slack:
|
||||
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
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install build-essential
|
||||
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
|
||||
RUN tar zxvf ta-lib-0.4.0-src.tar.gz
|
||||
RUN cd ta-lib && ./configure && make && make install
|
||||
# Install TA-lib
|
||||
RUN apt-get update && apt-get -y install build-essential && apt-get clean
|
||||
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
|
||||
tar xzvf - && \
|
||||
cd ta-lib && \
|
||||
./configure && make && make install && \
|
||||
cd .. && rm -rf ta-lib
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
RUN mkdir -p /freqtrade
|
||||
# Prepare environment
|
||||
RUN mkdir /freqtrade
|
||||
WORKDIR /freqtrade
|
||||
|
||||
ADD ./requirements.txt /freqtrade/requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
ADD . /freqtrade
|
||||
CMD python main.py
|
||||
# Install dependencies
|
||||
COPY requirements.txt /freqtrade/
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Install and execute
|
||||
COPY . /freqtrade/
|
||||
RUN pip install -e .
|
||||
CMD ["freqtrade"]
|
||||
|
5
MANIFEST.in
Normal file
5
MANIFEST.in
Normal file
@@ -0,0 +1,5 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include config.json.example
|
||||
recursive-include freqtrade *.py
|
||||
include freqtrade/tests/testdata/*.json
|
106
README.md
106
README.md
@@ -1,6 +1,8 @@
|
||||
# freqtrade
|
||||
|
||||
[](https://travis-ci.org/gcarq/freqtrade)
|
||||
[](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
|
||||
|
||||
|
||||
Simple High frequency trading bot for crypto currencies.
|
||||
Currently supports trading on Bittrex exchange.
|
||||
@@ -14,29 +16,29 @@ and enter the telegram `token` and your `chat_id` in `config.json`
|
||||
|
||||
Persistence is achieved through sqlite.
|
||||
|
||||
#### Telegram RPC commands:
|
||||
### Telegram RPC commands:
|
||||
* /start: Starts the trader
|
||||
* /stop: Stops the trader
|
||||
* /status: Lists all open trades
|
||||
* /status [table]: Lists all open trades
|
||||
* /count: Displays number of open trades
|
||||
* /profit: Lists cumulative profit from all finished trades
|
||||
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
* /performance: Show performance of each finished trade grouped by pair
|
||||
|
||||
#### Config
|
||||
### Config
|
||||
`minimal_roi` is a JSON object where the key is a duration
|
||||
in minutes and the value is the minimum ROI in percent.
|
||||
See the example below:
|
||||
```
|
||||
"minimal_roi": {
|
||||
"2880": 0.005, # Sell after 48 hours if there is at least 0.5% profit
|
||||
"1440": 0.01, # Sell after 24 hours if there is at least 1% profit
|
||||
"720": 0.02, # Sell after 12 hours if there is at least 2% profit
|
||||
"360": 0.02, # Sell after 6 hours if there is at least 2% profit
|
||||
"0": 0.025 # Sell immediately if there is at least 2.5% profit
|
||||
"50": 0.0, # Sell after 30 minutes if the profit is not negative
|
||||
"40": 0.01, # Sell after 25 minutes if there is at least 1% profit
|
||||
"30": 0.02, # Sell after 15 minutes if there is at least 2% profit
|
||||
"0": 0.045 # Sell immediately if there is at least 4.5% profit
|
||||
},
|
||||
```
|
||||
|
||||
`stoploss` is loss in percentage that should trigger a sale.
|
||||
`stoploss` is loss in percentage that should trigger a sale.
|
||||
For example value `-0.10` will cause immediate sell if the
|
||||
profit dips below -10% for a given trade. This parameter is optional.
|
||||
|
||||
@@ -45,17 +47,30 @@ Possible values are `running` or `stopped`. (default=`running`)
|
||||
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
|
||||
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,
|
||||
if not feel free to raise a github issue.
|
||||
|
||||
#### Prerequisites
|
||||
### Prerequisites
|
||||
* python3.6
|
||||
* sqlite
|
||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||
|
||||
#### Install
|
||||
### Install
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
Use your favorite AUR helper and install `python-freqtrade-git`.
|
||||
|
||||
#### Manually
|
||||
|
||||
`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/
|
||||
# copy example config. Dont forget to insert your api keys
|
||||
@@ -63,25 +78,74 @@ $ cp config.json.example config.json
|
||||
$ python -m venv .env
|
||||
$ source .env/bin/activate
|
||||
$ 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)).*
|
||||
|
||||
#### Execute tests
|
||||
|
||||
```
|
||||
$ python -m unittest
|
||||
```
|
||||
\* *Note:* that article was written for an earlier version, so it may be outdated
|
||||
|
||||
#### Docker
|
||||
|
||||
Building the image:
|
||||
|
||||
```
|
||||
$ cd freqtrade
|
||||
$ docker build -t freqtrade .
|
||||
$ docker run --rm -it freqtrade
|
||||
```
|
||||
|
||||
#### Contributing
|
||||
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.
|
||||
|
||||
### Execute tests
|
||||
|
||||
```
|
||||
$ pytest
|
||||
```
|
||||
This will by default skip the slow running backtest set. To run backtest set:
|
||||
|
||||
```
|
||||
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
||||
|
||||
|
163
analyze.py
163
analyze.py
@@ -1,163 +0,0 @@
|
||||
import time
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import arrow
|
||||
import requests
|
||||
from pandas import DataFrame
|
||||
import talib.abstract as ta
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
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:
|
||||
"""
|
||||
Analyses the trend for the given pair
|
||||
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
||||
:return: DataFrame
|
||||
"""
|
||||
df = DataFrame(ticker) \
|
||||
.drop('BV', 1) \
|
||||
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
|
||||
.sort_values('date')
|
||||
return df[df['date'].map(arrow.get) > minimum_date]
|
||||
|
||||
|
||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
"""
|
||||
dataframe['ema'] = ta.EMA(dataframe, timeperiod=33)
|
||||
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.22)
|
||||
dataframe['adx'] = ta.ADX(dataframe)
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy trend for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
: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['close'] > dataframe['sar']) &
|
||||
(prev_close > prev_sar) &
|
||||
(prev_close2 < prev_sar2),
|
||||
'swap'
|
||||
] = 1
|
||||
|
||||
# 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
|
||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def analyze_ticker(pair: str) -> DataFrame:
|
||||
"""
|
||||
Get ticker data for given currency pair, push it to a DataFrame and
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
minimum_date = arrow.utcnow().shift(hours=-6)
|
||||
data = get_ticker(pair, minimum_date)
|
||||
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
||||
dataframe = populate_indicators(dataframe)
|
||||
dataframe = populate_buy_trend(dataframe)
|
||||
return dataframe
|
||||
|
||||
def get_buy_signal(pair: str) -> bool:
|
||||
"""
|
||||
Calculates a buy signal based several technical analysis indicators
|
||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||
:return: True if pair is good for buying, False otherwise
|
||||
"""
|
||||
dataframe = analyze_ticker(pair)
|
||||
latest = dataframe.iloc[-1]
|
||||
|
||||
# Check if dataframe is out of date
|
||||
signal_date = arrow.get(latest['date'])
|
||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||
return False
|
||||
|
||||
signal = latest['buy'] == 1
|
||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
||||
return signal
|
||||
|
||||
|
||||
def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
||||
"""
|
||||
Plots the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:param pair: pair as str
|
||||
:return: None
|
||||
"""
|
||||
|
||||
import matplotlib
|
||||
|
||||
matplotlib.use("Qt5Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2) = plt.subplots(2, sharex=True)
|
||||
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['sell'], 'ro', label='sell')
|
||||
ax1.plot(dataframe.index.values, dataframe['ema'], '--', label='EMA(20)')
|
||||
ax1.plot(dataframe.index.values, dataframe['buy'], 'bo', label='buy')
|
||||
ax1.legend()
|
||||
|
||||
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
||||
ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
||||
ax2.legend()
|
||||
|
||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||
# all but bottom plot.
|
||||
fig.subplots_adjust(hspace=0)
|
||||
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||
while True:
|
||||
test_pair = 'BTC_ANT'
|
||||
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||
# get_buy_signal(pair)
|
||||
plot_dataframe(analyze_ticker(test_pair), test_pair)
|
||||
time.sleep(60)
|
4
bin/freqtrade
Executable file
4
bin/freqtrade
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from freqtrade.main import main
|
||||
main()
|
@@ -4,16 +4,17 @@
|
||||
"stake_amount": 0.05,
|
||||
"dry_run": false,
|
||||
"minimal_roi": {
|
||||
"2880": 0.005,
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
"50": 0.0,
|
||||
"40": 0.01,
|
||||
"30": 0.02,
|
||||
"0": 0.045
|
||||
},
|
||||
"stoploss": -0.10,
|
||||
"stoploss": -0.40,
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": true,
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"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.14.0'
|
||||
|
||||
from . import main
|
167
freqtrade/analyze.py
Normal file
167
freqtrade/analyze.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import logging
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
import arrow
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame, to_datetime
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.exchange import Bittrex, get_ticker_history
|
||||
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||
"""
|
||||
Analyses the trend for the given ticker history
|
||||
:param ticker: See exchange.get_ticker_history
|
||||
:return: DataFrame
|
||||
"""
|
||||
columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
|
||||
frame = DataFrame(ticker) \
|
||||
.drop('BV', 1) \
|
||||
.rename(columns=columns)
|
||||
frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
|
||||
frame.sort_values('date', inplace=True)
|
||||
return frame
|
||||
|
||||
|
||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Adds several different TA indicators to the given DataFrame
|
||||
"""
|
||||
dataframe['sar'] = ta.SAR(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)
|
||||
dataframe['rsi'] = ta.RSI(dataframe)
|
||||
dataframe['mom'] = ta.MOM(dataframe)
|
||||
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||
dataframe['ao'] = awesome_oscillator(dataframe)
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macdsignal'] = macd['macdsignal']
|
||||
dataframe['macdhist'] = macd['macdhist']
|
||||
return dataframe
|
||||
|
||||
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Based on TA indicators, populates the buy trend for the given dataframe
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with buy column
|
||||
"""
|
||||
dataframe.ix[
|
||||
(dataframe['close'] < dataframe['sma']) &
|
||||
(dataframe['tema'] <= dataframe['blower']) &
|
||||
(dataframe['mfi'] < 25) &
|
||||
(dataframe['fastd'] < 25) &
|
||||
(dataframe['adx'] > 30),
|
||||
'buy'] = 1
|
||||
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def analyze_ticker(pair: str) -> DataFrame:
|
||||
"""
|
||||
Get ticker data for given currency pair, push it to a DataFrame and
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
data = get_ticker_history(pair)
|
||||
dataframe = parse_ticker_dataframe(data)
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return dataframe
|
||||
|
||||
dataframe = populate_indicators(dataframe)
|
||||
dataframe = populate_buy_trend(dataframe)
|
||||
return dataframe
|
||||
|
||||
|
||||
def get_buy_signal(pair: str) -> bool:
|
||||
"""
|
||||
Calculates a buy signal based several technical analysis indicators
|
||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||
:return: True if pair is good for buying, False otherwise
|
||||
"""
|
||||
dataframe = analyze_ticker(pair)
|
||||
|
||||
if dataframe.empty:
|
||||
return False
|
||||
|
||||
latest = dataframe.iloc[-1]
|
||||
|
||||
# Check if dataframe is out of date
|
||||
signal_date = arrow.get(latest['date'])
|
||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||
return False
|
||||
|
||||
signal = latest['buy'] == 1
|
||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
||||
return signal
|
||||
|
||||
|
||||
def plot_analyzed_dataframe(pair: str) -> None:
|
||||
"""
|
||||
Calls analyze() and plots the returned dataframe
|
||||
:param pair: pair as str
|
||||
:return: None
|
||||
"""
|
||||
import matplotlib
|
||||
matplotlib.use("Qt5Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Init Bittrex to use public API
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
dataframe = analyze_ticker(pair)
|
||||
|
||||
# Two subplots sharing x axis
|
||||
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
|
||||
fig.suptitle(pair, fontsize=14, fontweight='bold')
|
||||
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
|
||||
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
|
||||
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
|
||||
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
|
||||
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
|
||||
ax1.legend()
|
||||
|
||||
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
|
||||
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
|
||||
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
|
||||
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
|
||||
# all but bottom plot.
|
||||
fig.subplots_adjust(hspace=0)
|
||||
plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False)
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||
while True:
|
||||
for p in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||
plot_analyzed_dataframe(p)
|
||||
time.sleep(60)
|
164
freqtrade/exchange/__init__.py
Normal file
164
freqtrade/exchange/__init__.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import enum
|
||||
import logging
|
||||
from random import randint
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
import arrow
|
||||
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Current selected exchange
|
||||
_API: Exchange = None
|
||||
_CONF: dict = {}
|
||||
|
||||
# Holds all open sell orders for dry_run
|
||||
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
|
||||
|
||||
|
||||
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, _API
|
||||
|
||||
_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))
|
||||
|
||||
_API = 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 = _API.get_markets()
|
||||
for pair in pairs:
|
||||
if pair not in markets:
|
||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
||||
|
||||
|
||||
def buy(pair: str, rate: float, amount: float) -> str:
|
||||
if _CONF['dry_run']:
|
||||
global _DRY_RUN_OPEN_ORDERS
|
||||
order_id = 'dry_run_buy_{}'.format(randint(0, 1e6))
|
||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||
'pair': pair,
|
||||
'rate': rate,
|
||||
'amount': amount,
|
||||
'type': 'LIMIT_BUY',
|
||||
'remaining': 0.0,
|
||||
'opened': arrow.utcnow().datetime,
|
||||
'closed': arrow.utcnow().datetime,
|
||||
}
|
||||
return order_id
|
||||
|
||||
return _API.buy(pair, rate, amount)
|
||||
|
||||
|
||||
def sell(pair: str, rate: float, amount: float) -> str:
|
||||
if _CONF['dry_run']:
|
||||
global _DRY_RUN_OPEN_ORDERS
|
||||
order_id = 'dry_run_sell_{}'.format(randint(0, 1e6))
|
||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||
'pair': pair,
|
||||
'rate': rate,
|
||||
'amount': amount,
|
||||
'type': 'LIMIT_SELL',
|
||||
'remaining': 0.0,
|
||||
'opened': arrow.utcnow().datetime,
|
||||
'closed': arrow.utcnow().datetime,
|
||||
}
|
||||
return order_id
|
||||
|
||||
return _API.sell(pair, rate, amount)
|
||||
|
||||
|
||||
def get_balance(currency: str) -> float:
|
||||
if _CONF['dry_run']:
|
||||
return 999.9
|
||||
|
||||
return _API.get_balance(currency)
|
||||
|
||||
|
||||
def get_balances():
|
||||
if _CONF['dry_run']:
|
||||
return []
|
||||
|
||||
return _API.get_balances()
|
||||
|
||||
|
||||
def get_ticker(pair: str) -> dict:
|
||||
return _API.get_ticker(pair)
|
||||
|
||||
|
||||
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List:
|
||||
return _API.get_ticker_history(pair, tick_interval)
|
||||
|
||||
|
||||
def cancel_order(order_id: str) -> None:
|
||||
if _CONF['dry_run']:
|
||||
return
|
||||
|
||||
return _API.cancel_order(order_id)
|
||||
|
||||
|
||||
def get_order(order_id: str) -> Dict:
|
||||
if _CONF['dry_run']:
|
||||
order = _DRY_RUN_OPEN_ORDERS[order_id]
|
||||
order.update({
|
||||
'id': order_id
|
||||
})
|
||||
return order
|
||||
|
||||
return _API.get_order(order_id)
|
||||
|
||||
|
||||
def get_pair_detail_url(pair: str) -> str:
|
||||
return _API.get_pair_detail_url(pair)
|
||||
|
||||
|
||||
def get_markets() -> List[str]:
|
||||
return _API.get_markets()
|
||||
|
||||
|
||||
def get_name() -> str:
|
||||
return _API.name
|
||||
|
||||
|
||||
def get_sleep_time() -> float:
|
||||
return _API.sleep_time
|
||||
|
||||
|
||||
def get_fee() -> float:
|
||||
return _API.fee
|
141
freqtrade/exchange/bittrex.py
Normal file
141
freqtrade/exchange/bittrex.py
Normal file
@@ -0,0 +1,141 @@
|
||||
import logging
|
||||
from typing import List, Dict
|
||||
|
||||
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'
|
||||
|
||||
@property
|
||||
def sleep_time(self) -> float:
|
||||
""" Sleep time to avoid rate limits, used in the main loop """
|
||||
return 25
|
||||
|
||||
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'],
|
||||
calls_per_second=5,
|
||||
)
|
||||
|
||||
@property
|
||||
def fee(self) -> float:
|
||||
# See https://bittrex.com/fees
|
||||
return 0.0025
|
||||
|
||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
|
||||
message=data['message'],
|
||||
pair=pair,
|
||||
rate=rate,
|
||||
amount=amount))
|
||||
return data['result']['uuid']
|
||||
|
||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
|
||||
message=data['message'],
|
||||
pair=pair,
|
||||
rate=rate,
|
||||
amount=amount))
|
||||
return data['result']['uuid']
|
||||
|
||||
def get_balance(self, currency: str) -> float:
|
||||
data = _API.get_balance(currency)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({currency})'.format(
|
||||
message=data['message'],
|
||||
currency=currency))
|
||||
return float(data['result']['Balance'] or 0.0)
|
||||
|
||||
def get_balances(self):
|
||||
data = _API.get_balances()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message}'.format(message=data['message']))
|
||||
return data['result']
|
||||
|
||||
def get_ticker(self, pair: str) -> dict:
|
||||
data = _API.get_ticker(pair.replace('_', '-'))
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
pair=pair))
|
||||
return {
|
||||
'bid': float(data['result']['Bid']),
|
||||
'ask': float(data['result']['Ask']),
|
||||
'last': float(data['result']['Last']),
|
||||
}
|
||||
|
||||
def get_ticker_history(self, pair: str, tick_interval: int):
|
||||
if tick_interval == 1:
|
||||
interval = 'oneMin'
|
||||
elif tick_interval == 5:
|
||||
interval = 'fiveMin'
|
||||
else:
|
||||
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
|
||||
|
||||
data = requests.get(self.TICKER_METHOD, params={
|
||||
'marketName': pair.replace('_', '-'),
|
||||
'tickInterval': interval,
|
||||
}).json()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({pair})'.format(
|
||||
message=data['message'],
|
||||
pair=pair))
|
||||
|
||||
return data['result']
|
||||
|
||||
def get_order(self, order_id: str) -> Dict:
|
||||
data = _API.get_order(order_id)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({order_id})'.format(
|
||||
message=data['message'],
|
||||
order_id=order_id))
|
||||
data = data['result']
|
||||
return {
|
||||
'id': data['OrderUuid'],
|
||||
'type': data['Type'],
|
||||
'pair': data['Exchange'].replace('-', '_'),
|
||||
'opened': data['Opened'],
|
||||
'rate': data['PricePerUnit'],
|
||||
'amount': data['Quantity'],
|
||||
'remaining': data['QuantityRemaining'],
|
||||
'closed': data['Closed'],
|
||||
}
|
||||
|
||||
def cancel_order(self, order_id: str) -> None:
|
||||
data = _API.cancel(order_id)
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message} params=({order_id})'.format(
|
||||
message=data['message'],
|
||||
order_id=order_id))
|
||||
|
||||
def get_pair_detail_url(self, pair: str) -> str:
|
||||
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
|
||||
|
||||
def get_markets(self) -> List[str]:
|
||||
data = _API.get_markets()
|
||||
if not data['success']:
|
||||
raise RuntimeError('{message}'.format(message=data['message']))
|
||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
141
freqtrade/exchange/interface.py
Normal file
141
freqtrade/exchange/interface.py
Normal file
@@ -0,0 +1,141 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
class Exchange(ABC):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""
|
||||
Name of the exchange.
|
||||
:return: str representation of the class name
|
||||
"""
|
||||
return self.__class__.__name__
|
||||
|
||||
@property
|
||||
def fee(self) -> float:
|
||||
"""
|
||||
Fee for placing an order
|
||||
:return: percentage in float
|
||||
"""
|
||||
|
||||
@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_balances(self) -> List[dict]:
|
||||
"""
|
||||
Gets account balances across currencies
|
||||
:return: List of dicts, format: [
|
||||
{
|
||||
'Currency': str,
|
||||
'Balance': float,
|
||||
'Available': float,
|
||||
'Pending': float,
|
||||
}
|
||||
...
|
||||
]
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_ticker(self, pair: str) -> 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, tick_interval: int) -> List:
|
||||
"""
|
||||
Gets ticker history for given pair.
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
:param tick_interval: ticker interval in minutes
|
||||
:return: list, format: [
|
||||
{
|
||||
'O': float, (Open)
|
||||
'H': float, (High)
|
||||
'L': float, (Low)
|
||||
'C': float, (Close)
|
||||
'V': float, (Volume)
|
||||
'T': datetime, (Time)
|
||||
'BV': float, (Base Volume)
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
|
||||
def get_order(self, order_id: str) -> Dict:
|
||||
"""
|
||||
Get order details for the given order_id.
|
||||
:param order_id: ID as str
|
||||
:return: dict, format: {
|
||||
'id': str,
|
||||
'type': str,
|
||||
'pair': str,
|
||||
'opened': str ISO 8601 datetime,
|
||||
'closed': str ISO 8601 datetime,
|
||||
'rate': float,
|
||||
'amount': float,
|
||||
'remaining': int
|
||||
}
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def cancel_order(self, order_id: str) -> None:
|
||||
"""
|
||||
Cancels order for given order_id.
|
||||
:param order_id: ID as str
|
||||
:return: None
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_pair_detail_url(self, pair: str) -> str:
|
||||
"""
|
||||
Returns the market detail url for the given pair.
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
:return: URL as str
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_markets(self) -> List[str]:
|
||||
"""
|
||||
Returns all available markets.
|
||||
:return: List of all available pairs
|
||||
"""
|
@@ -1,69 +1,75 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
||||
|
||||
import requests
|
||||
from jsonschema import validate
|
||||
|
||||
import exchange
|
||||
import persistence
|
||||
from persistence import Trade
|
||||
from analyze import get_buy_signal
|
||||
from misc import CONF_SCHEMA, get_state, State, update_state
|
||||
from rpc import telegram
|
||||
from freqtrade import __version__, exchange, persistence
|
||||
from freqtrade.analyze import get_buy_signal
|
||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import telegram
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__author__ = "gcarq"
|
||||
__copyright__ = "gcarq 2017"
|
||||
__license__ = "GPLv3"
|
||||
__version__ = "0.10.0"
|
||||
|
||||
_CONF = {}
|
||||
|
||||
|
||||
def _process() -> None:
|
||||
def _process() -> bool:
|
||||
"""
|
||||
Queries the persistence layer for open trades and handles them,
|
||||
otherwise a new trade is created.
|
||||
:return: None
|
||||
:return: True if a trade has been created or closed, False otherwise
|
||||
"""
|
||||
state_changed = False
|
||||
try:
|
||||
# Query trades from persistence layer
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if len(trades) < _CONF['max_open_trades']:
|
||||
try:
|
||||
# Create entity and execute trade
|
||||
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE)
|
||||
trade = create_trade(float(_CONF['stake_amount']))
|
||||
if trade:
|
||||
Trade.session.add(trade)
|
||||
state_changed = True
|
||||
else:
|
||||
logging.info('Got no buy signal...')
|
||||
except ValueError:
|
||||
logger.exception('Unable to create trade')
|
||||
|
||||
for trade in trades:
|
||||
# Check if there is already an open order for this trade
|
||||
orders = exchange.get_open_orders(trade.pair)
|
||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
||||
if orders:
|
||||
logger.info('There is an open order for: %s', orders[0])
|
||||
else:
|
||||
# Update state
|
||||
trade.open_order_id = None
|
||||
# Check if this trade can be closed
|
||||
if not close_trade_if_fulfilled(trade):
|
||||
# Check if we can sell our current pair
|
||||
handle_trade(trade)
|
||||
Trade.session.flush()
|
||||
except (ConnectionError, json.JSONDecodeError) as error:
|
||||
msg = 'Got {} in _process()'.format(error.__class__.__name__)
|
||||
# Get order details for actual price per unit
|
||||
if trade.open_order_id:
|
||||
# Update trade with order values
|
||||
logger.info('Got open order for %s', trade)
|
||||
trade.update(exchange.get_order(trade.open_order_id))
|
||||
|
||||
if not close_trade_if_fulfilled(trade):
|
||||
# Check if we can sell our current pair
|
||||
state_changed = handle_trade(trade) or state_changed
|
||||
|
||||
Trade.session.flush()
|
||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
||||
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
|
||||
logger.exception(msg)
|
||||
time.sleep(30)
|
||||
except RuntimeError:
|
||||
telegram.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
||||
traceback=traceback.format_exc(),
|
||||
hint='Issue `/start` if you think it is safe to restart.'
|
||||
))
|
||||
logger.exception('Got RuntimeError. Stopping trader ...')
|
||||
update_state(State.STOPPED)
|
||||
return state_changed
|
||||
|
||||
|
||||
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
||||
@@ -84,26 +90,24 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def execute_sell(trade: Trade, current_rate: float) -> None:
|
||||
def execute_sell(trade: Trade, limit: float) -> None:
|
||||
"""
|
||||
Executes a sell for the given trade and current rate
|
||||
Executes a limit sell for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
:param current_rate: current rate
|
||||
:param limit: limit rate for the sell order
|
||||
:return: None
|
||||
"""
|
||||
# Get available balance
|
||||
currency = trade.pair.split('_')[1]
|
||||
balance = exchange.get_balance(currency)
|
||||
whitelist = _CONF[trade.exchange.name.lower()]['pair_whitelist']
|
||||
# Execute sell and update trade record
|
||||
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
||||
trade.open_order_id = order_id
|
||||
|
||||
profit = trade.exec_sell_order(current_rate, balance)
|
||||
whitelist.append(trade.pair)
|
||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||
trade.exchange.name,
|
||||
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
||||
message = '*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
||||
trade.exchange,
|
||||
trade.pair.replace('_', '/'),
|
||||
exchange.get_pair_detail_url(trade.pair),
|
||||
trade.close_rate,
|
||||
round(profit, 2)
|
||||
limit,
|
||||
fmt_exp_profit
|
||||
)
|
||||
logger.info(message)
|
||||
telegram.send_msg(message)
|
||||
@@ -114,41 +118,36 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
|
||||
Based an earlier trade and current price and configuration, decides whether bot should sell
|
||||
:return True if bot should sell at current rate
|
||||
"""
|
||||
current_profit = (current_rate - trade.open_rate) / trade.open_rate
|
||||
|
||||
current_profit = trade.calc_profit(current_rate)
|
||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
||||
logger.debug('Stop loss hit.')
|
||||
return True
|
||||
|
||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||
duration, threshold = float(duration), float(threshold)
|
||||
# Check if time matches and current rate is above threshold
|
||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
||||
if time_diff > duration and current_profit > threshold:
|
||||
if time_diff > float(duration) and current_profit > threshold:
|
||||
return True
|
||||
|
||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
|
||||
return False
|
||||
|
||||
|
||||
def handle_trade(trade: Trade) -> None:
|
||||
def handle_trade(trade: Trade) -> bool:
|
||||
"""
|
||||
Sells the current pair if the threshold is reached and updates the trade record.
|
||||
:return: None
|
||||
:return: True if trade has been sold, False otherwise
|
||||
"""
|
||||
try:
|
||||
if not trade.is_open:
|
||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
||||
if not trade.is_open:
|
||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
||||
|
||||
logger.debug('Handling open trade %s ...', trade)
|
||||
logger.debug('Handling %s ...', trade)
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
||||
execute_sell(trade, current_rate)
|
||||
return True
|
||||
return False
|
||||
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
||||
execute_sell(trade, current_rate)
|
||||
return
|
||||
|
||||
except ValueError:
|
||||
logger.exception('Unable to handle open order')
|
||||
|
||||
def get_target_bid(ticker: Dict[str, float]) -> float:
|
||||
""" Calculates bid target between current ask price and last price """
|
||||
@@ -158,27 +157,22 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
|
||||
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,
|
||||
if one pair triggers the buy_signal a new trade record gets created
|
||||
:param stake_amount: amount of btc to spend
|
||||
:param _exchange: exchange to use
|
||||
"""
|
||||
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
|
||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||
raise ValueError(
|
||||
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
|
||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||
)
|
||||
|
||||
# Remove currently opened and latest pairs from whitelist
|
||||
trades = 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:
|
||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||
if trade.pair in whitelist:
|
||||
whitelist.remove(trade.pair)
|
||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||
@@ -193,27 +187,30 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
||||
else:
|
||||
return None
|
||||
|
||||
open_rate = get_target_bid(exchange.get_ticker(pair))
|
||||
amount = stake_amount / open_rate
|
||||
order_id = exchange.buy(pair, open_rate, amount)
|
||||
# Calculate amount and subtract fee
|
||||
fee = exchange.get_fee()
|
||||
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
||||
amount = (1 - fee) * stake_amount / buy_limit
|
||||
|
||||
order_id = exchange.buy(pair, buy_limit, amount)
|
||||
# Create trade entity and return
|
||||
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
||||
_exchange.name,
|
||||
message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
|
||||
exchange.get_name().upper(),
|
||||
pair.replace('_', '/'),
|
||||
exchange.get_pair_detail_url(pair),
|
||||
open_rate
|
||||
buy_limit
|
||||
)
|
||||
logger.info(message)
|
||||
telegram.send_msg(message)
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
return Trade(pair=pair,
|
||||
stake_amount=stake_amount,
|
||||
open_rate=open_rate,
|
||||
open_date=datetime.utcnow(),
|
||||
amount=amount,
|
||||
exchange=_exchange,
|
||||
open_order_id=order_id,
|
||||
is_open=True)
|
||||
fee=fee * 2,
|
||||
open_rate=buy_limit,
|
||||
open_date=datetime.utcnow(),
|
||||
exchange=exchange.get_name().upper(),
|
||||
open_order_id=order_id)
|
||||
|
||||
|
||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
@@ -235,42 +232,57 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
else:
|
||||
update_state(State.STOPPED)
|
||||
|
||||
# Register signal handlers
|
||||
for sig in (SIGINT, SIGTERM, SIGABRT):
|
||||
signal(sig, cleanup)
|
||||
|
||||
def app(config: dict) -> None:
|
||||
|
||||
def cleanup(*args, **kwargs) -> None:
|
||||
"""
|
||||
Main function which handles the application state
|
||||
:param config: config as dict
|
||||
Cleanup the application state und finish all pending tasks
|
||||
:return: None
|
||||
"""
|
||||
telegram.send_msg('*Status:* `Stopping trader...`')
|
||||
logger.info('Stopping trader and cleaning up modules...')
|
||||
update_state(State.STOPPED)
|
||||
persistence.cleanup()
|
||||
telegram.cleanup()
|
||||
exit(0)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Loads and validates the config and handles the main loop
|
||||
:return: None
|
||||
"""
|
||||
logger.info('Starting freqtrade %s', __version__)
|
||||
init(config)
|
||||
try:
|
||||
old_state = get_state()
|
||||
logger.info('Initial State: %s', old_state)
|
||||
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
|
||||
while True:
|
||||
new_state = get_state()
|
||||
# Log state transition
|
||||
if new_state != old_state:
|
||||
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||
logging.info('Changing state to: %s', new_state.name)
|
||||
|
||||
if new_state == State.STOPPED:
|
||||
time.sleep(1)
|
||||
elif new_state == State.RUNNING:
|
||||
_process()
|
||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
||||
time.sleep(25)
|
||||
old_state = new_state
|
||||
except RuntimeError:
|
||||
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
||||
logger.exception('RuntimeError. Trader stopped!')
|
||||
finally:
|
||||
telegram.send_msg('*Status:* `Trader has stopped`')
|
||||
global _CONF
|
||||
with open('config.json') as file:
|
||||
_CONF = json.load(file)
|
||||
|
||||
logger.info('Validating configuration ...')
|
||||
validate(_CONF, CONF_SCHEMA)
|
||||
|
||||
init(_CONF)
|
||||
old_state = get_state()
|
||||
logger.info('Initial State: %s', old_state)
|
||||
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
|
||||
while True:
|
||||
new_state = get_state()
|
||||
# Log state transition
|
||||
if new_state != old_state:
|
||||
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||
logging.info('Changing state to: %s', new_state.name)
|
||||
|
||||
if new_state == State.STOPPED:
|
||||
time.sleep(1)
|
||||
elif new_state == State.RUNNING:
|
||||
_process()
|
||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
||||
time.sleep(exchange.get_sleep_time())
|
||||
old_state = new_state
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with open('config.json') as file:
|
||||
_CONF = json.load(file)
|
||||
validate(_CONF, CONF_SCHEMA)
|
||||
app(_CONF)
|
||||
main()
|
@@ -60,7 +60,7 @@ CONF_SCHEMA = {
|
||||
},
|
||||
'required': ['ask_last_balance']
|
||||
},
|
||||
'bittrex': {'$ref': '#/definitions/exchange'},
|
||||
'exchange': {'$ref': '#/definitions/exchange'},
|
||||
'telegram': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
@@ -76,7 +76,7 @@ CONF_SCHEMA = {
|
||||
'exchange': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'name': {'type': 'string'},
|
||||
'key': {'type': 'string'},
|
||||
'secret': {'type': 'string'},
|
||||
'pair_whitelist': {
|
||||
@@ -85,11 +85,11 @@ CONF_SCHEMA = {
|
||||
'uniqueItems': True
|
||||
}
|
||||
},
|
||||
'required': ['enabled', 'key', 'secret', 'pair_whitelist']
|
||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||
}
|
||||
},
|
||||
'anyOf': [
|
||||
{'required': ['bittrex']}
|
||||
{'required': ['exchange']}
|
||||
],
|
||||
'required': [
|
||||
'max_open_trades',
|
110
freqtrade/persistence.py
Normal file
110
freqtrade/persistence.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, getcontext
|
||||
from typing import Optional, Dict
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CONF = {}
|
||||
_DECL_BASE = declarative_base()
|
||||
|
||||
|
||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
registers all known command handlers
|
||||
and starts polling for message updates
|
||||
:param config: config to use
|
||||
:param db_url: database connector string for sqlalchemy (Optional)
|
||||
:return: None
|
||||
"""
|
||||
_CONF.update(config)
|
||||
if not db_url:
|
||||
if _CONF.get('dry_run', False):
|
||||
db_url = 'sqlite://'
|
||||
else:
|
||||
db_url = 'sqlite:///tradesv3.sqlite'
|
||||
|
||||
engine = create_engine(db_url, echo=False)
|
||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||
Trade.session = session()
|
||||
Trade.query = session.query_property()
|
||||
_DECL_BASE.metadata.create_all(engine)
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
"""
|
||||
Flushes all pending operations to disk.
|
||||
:return: None
|
||||
"""
|
||||
Trade.session.flush()
|
||||
|
||||
|
||||
class Trade(_DECL_BASE):
|
||||
__tablename__ = 'trades'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
exchange = Column(String, nullable=False)
|
||||
pair = Column(String, nullable=False)
|
||||
is_open = Column(Boolean, nullable=False, default=True)
|
||||
fee = Column(Float, nullable=False, default=0.0)
|
||||
open_rate = Column(Float)
|
||||
close_rate = Column(Float)
|
||||
close_profit = Column(Float)
|
||||
stake_amount = Column(Float, nullable=False)
|
||||
amount = Column(Float)
|
||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
close_date = Column(DateTime)
|
||||
open_order_id = Column(String)
|
||||
|
||||
def __repr__(self):
|
||||
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
|
||||
self.id,
|
||||
self.pair,
|
||||
self.amount,
|
||||
self.open_rate,
|
||||
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||
)
|
||||
|
||||
def update(self, order: Dict) -> None:
|
||||
"""
|
||||
Updates this entity with amount and actual open/close rates.
|
||||
:param order: order retrieved by exchange.get_order()
|
||||
:return: None
|
||||
"""
|
||||
if not order['closed']:
|
||||
return
|
||||
|
||||
logger.debug('Updating trade (id=%d) ...', self.id)
|
||||
if order['type'] == 'LIMIT_BUY':
|
||||
# Update open rate and actual amount
|
||||
self.open_rate = order['rate']
|
||||
self.amount = order['amount']
|
||||
elif order['type'] == 'LIMIT_SELL':
|
||||
# Set close rate and set actual profit
|
||||
self.close_rate = order['rate']
|
||||
self.close_profit = self.calc_profit()
|
||||
self.close_date = datetime.utcnow()
|
||||
else:
|
||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||
|
||||
self.open_order_id = None
|
||||
|
||||
def calc_profit(self, rate: Optional[float] = None) -> float:
|
||||
"""
|
||||
Calculates the profit in percentage (including fee).
|
||||
:param rate: rate to compare with (optional).
|
||||
If rate is not set self.close_rate will be used
|
||||
:return: profit in percentage as float
|
||||
"""
|
||||
getcontext().prec = 8
|
||||
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
|
||||
/ Decimal(self.open_rate) - Decimal(self.fee))
|
@@ -1,24 +1,26 @@
|
||||
import logging
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from typing import Callable, Any
|
||||
from pandas import DataFrame
|
||||
from tabulate import tabulate
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import and_, func, text
|
||||
from telegram import ParseMode, Bot, Update
|
||||
from telegram.error import NetworkError
|
||||
from telegram.ext import CommandHandler, Updater
|
||||
from telegram import ParseMode, Bot, Update
|
||||
|
||||
from misc import get_state, State, update_state
|
||||
from persistence import Trade
|
||||
|
||||
import exchange
|
||||
from freqtrade import exchange
|
||||
from freqtrade.misc import get_state, State, update_state
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
# Remove noisy log messages
|
||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_updater = None
|
||||
_UPDATER: Updater = None
|
||||
_CONF = {}
|
||||
|
||||
|
||||
@@ -30,23 +32,29 @@ def init(config: dict) -> None:
|
||||
:param config: config to use
|
||||
:return: None
|
||||
"""
|
||||
global _updater
|
||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||
global _UPDATER
|
||||
|
||||
_CONF.update(config)
|
||||
if not is_enabled():
|
||||
return
|
||||
|
||||
_UPDATER = Updater(token=config['telegram']['token'], workers=0)
|
||||
|
||||
# Register command handler and start telegram message polling
|
||||
handles = [
|
||||
CommandHandler('status', _status),
|
||||
CommandHandler('profit', _profit),
|
||||
CommandHandler('balance', _balance),
|
||||
CommandHandler('start', _start),
|
||||
CommandHandler('stop', _stop),
|
||||
CommandHandler('forcesell', _forcesell),
|
||||
CommandHandler('performance', _performance),
|
||||
CommandHandler('count', _count),
|
||||
CommandHandler('help', _help),
|
||||
]
|
||||
for handle in handles:
|
||||
_updater.dispatcher.add_handler(handle)
|
||||
_updater.start_polling(
|
||||
_UPDATER.dispatcher.add_handler(handle)
|
||||
_UPDATER.start_polling(
|
||||
clean=True,
|
||||
bootstrap_retries=3,
|
||||
timeout=30,
|
||||
@@ -58,6 +66,23 @@ def init(config: dict) -> None:
|
||||
)
|
||||
|
||||
|
||||
def cleanup() -> None:
|
||||
"""
|
||||
Stops all running telegram threads.
|
||||
:return: None
|
||||
"""
|
||||
if not is_enabled():
|
||||
return
|
||||
_UPDATER.stop()
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
"""
|
||||
Returns True if the telegram module is activated, False otherwise
|
||||
"""
|
||||
return bool(_CONF['telegram'].get('enabled', False))
|
||||
|
||||
|
||||
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
||||
"""
|
||||
Decorator to check if the message comes from the correct chat_id
|
||||
@@ -67,15 +92,17 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
|
||||
def wrapper(*args, **kwargs):
|
||||
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
|
||||
|
||||
if not isinstance(bot, Bot) or not isinstance(update, Update):
|
||||
raise ValueError('Received invalid Arguments: {}'.format(*args))
|
||||
|
||||
# Reject unauthorized messages
|
||||
chat_id = int(_CONF['telegram']['chat_id'])
|
||||
if int(update.message.chat_id) == chat_id:
|
||||
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
||||
return command_handler(*args, **kwargs)
|
||||
else:
|
||||
if int(update.message.chat_id) != chat_id:
|
||||
logger.info('Rejected unauthorized message from: %s', update.message.chat_id)
|
||||
return wrapper
|
||||
|
||||
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
|
||||
try:
|
||||
return command_handler(*args, **kwargs)
|
||||
except BaseException:
|
||||
logger.exception('Exception occurred within Telegram module')
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -88,32 +115,39 @@ def _status(bot: Bot, update: Update) -> None:
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
|
||||
# Check if additional parameters are passed
|
||||
params = update.message.text.replace('/status', '').split(' ') \
|
||||
if update.message.text else []
|
||||
if 'table' in params:
|
||||
_status_table(bot, update)
|
||||
return
|
||||
|
||||
# Fetch open trade
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if get_state() != State.RUNNING:
|
||||
send_msg('*Status:* `trader is not running`', bot=bot)
|
||||
elif not trades:
|
||||
send_msg('*Status:* `no active order`', bot=bot)
|
||||
send_msg('*Status:* `no active trade`', bot=bot)
|
||||
else:
|
||||
for trade in trades:
|
||||
order = None
|
||||
if trade.open_order_id:
|
||||
order = exchange.get_order(trade.open_order_id)
|
||||
# calculate profit and send message to user
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||
orders = exchange.get_open_orders(trade.pair)
|
||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
||||
order = orders[0] if orders else None
|
||||
|
||||
current_profit = trade.calc_profit(current_rate)
|
||||
fmt_close_profit = '{:.2f}%'.format(
|
||||
round(trade.close_profit, 2)
|
||||
round(trade.close_profit * 100, 2)
|
||||
) if trade.close_profit else None
|
||||
message = """
|
||||
*Trade ID:* `{trade_id}`
|
||||
*Current Pair:* [{pair}]({market_url})
|
||||
*Open Since:* `{date}`
|
||||
*Amount:* `{amount}`
|
||||
*Open Rate:* `{open_rate}`
|
||||
*Open Rate:* `{open_rate:.8f}`
|
||||
*Close Rate:* `{close_rate}`
|
||||
*Current Rate:* `{current_rate}`
|
||||
*Current Rate:* `{current_rate:.8f}`
|
||||
*Close Profit:* `{close_profit}`
|
||||
*Current Profit:* `{current_profit:.2f}%`
|
||||
*Open Order:* `{open_order}`
|
||||
@@ -127,12 +161,51 @@ def _status(bot: Bot, update: Update) -> None:
|
||||
current_rate=current_rate,
|
||||
amount=round(trade.amount, 8),
|
||||
close_profit=fmt_close_profit,
|
||||
current_profit=round(current_profit, 2),
|
||||
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None,
|
||||
current_profit=round(current_profit * 100, 2),
|
||||
open_order='{} ({})'.format(
|
||||
order['remaining'], order['type']
|
||||
) if order else None,
|
||||
)
|
||||
send_msg(message, bot=bot)
|
||||
|
||||
|
||||
@authorized_only
|
||||
def _status_table(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /status table.
|
||||
Returns the current TradeThread status in table format
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
# Fetch open trade
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if get_state() != State.RUNNING:
|
||||
send_msg('*Status:* `trader is not running`', bot=bot)
|
||||
elif not trades:
|
||||
send_msg('*Status:* `no active order`', bot=bot)
|
||||
else:
|
||||
trades_list = []
|
||||
for trade in trades:
|
||||
# calculate profit and send message to user
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
trades_list.append([
|
||||
trade.id,
|
||||
trade.pair,
|
||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||
'{:.2f}'.format(100 * trade.calc_profit(current_rate))
|
||||
])
|
||||
|
||||
columns = ['ID', 'Pair', 'Since', 'Profit']
|
||||
df_statuses = DataFrame.from_records(trades_list, columns=columns)
|
||||
df_statuses = df_statuses.set_index(columns[0])
|
||||
|
||||
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
|
||||
message = "<pre>{}</pre>".format(message)
|
||||
|
||||
send_msg(message, parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@authorized_only
|
||||
def _profit(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
@@ -148,6 +221,8 @@ def _profit(bot: Bot, update: Update) -> None:
|
||||
profits = []
|
||||
durations = []
|
||||
for trade in trades:
|
||||
if not trade.open_rate:
|
||||
continue
|
||||
if trade.close_date:
|
||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||
if trade.close_profit:
|
||||
@@ -155,9 +230,9 @@ def _profit(bot: Bot, update: Update) -> None:
|
||||
else:
|
||||
# Get current rate
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||
profit = trade.calc_profit(current_rate)
|
||||
|
||||
profit_amounts.append((profit / 100) * trade.stake_amount)
|
||||
profit_amounts.append(profit * trade.stake_amount)
|
||||
profits.append(profit)
|
||||
|
||||
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||
@@ -172,7 +247,7 @@ def _profit(bot: Bot, update: Update) -> None:
|
||||
|
||||
bp_pair, bp_rate = best_pair
|
||||
markdown_msg = """
|
||||
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)`
|
||||
*ROI:* `{profit_btc:.8f} ({profit:.2f}%)`
|
||||
*Trade Count:* `{trade_count}`
|
||||
*First Trade opened:* `{first_trade_date}`
|
||||
*Latest Trade opened:* `{latest_trade_date}`
|
||||
@@ -180,17 +255,38 @@ def _profit(bot: Bot, update: Update) -> None:
|
||||
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
||||
""".format(
|
||||
profit_btc=round(sum(profit_amounts), 8),
|
||||
profit=round(sum(profits), 2),
|
||||
profit=round(sum(profits) * 100, 2),
|
||||
trade_count=len(trades),
|
||||
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
||||
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
||||
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
|
||||
best_pair=bp_pair,
|
||||
best_rate=round(bp_rate, 2),
|
||||
best_rate=round(bp_rate * 100, 2),
|
||||
)
|
||||
send_msg(markdown_msg, bot=bot)
|
||||
|
||||
|
||||
@authorized_only
|
||||
def _balance(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /balance
|
||||
Returns current account balance per crypto
|
||||
"""
|
||||
output = ""
|
||||
balances = exchange.get_balances()
|
||||
for currency in balances:
|
||||
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
|
||||
continue
|
||||
output += """*Currency*: {Currency}
|
||||
*Available*: {Available}
|
||||
*Balance*: {Balance}
|
||||
*Pending*: {Pending}
|
||||
|
||||
""".format(**currency)
|
||||
|
||||
send_msg(output)
|
||||
|
||||
|
||||
@authorized_only
|
||||
def _start(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
@@ -249,20 +345,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
||||
return
|
||||
# Get current rate
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
# Get available balance
|
||||
currency = trade.pair.split('_')[1]
|
||||
balance = exchange.get_balance(currency)
|
||||
# Execute sell
|
||||
profit = trade.exec_sell_order(current_rate, balance)
|
||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||
trade.exchange.name,
|
||||
trade.pair.replace('_', '/'),
|
||||
exchange.get_pair_detail_url(trade.pair),
|
||||
trade.close_rate,
|
||||
round(profit, 2)
|
||||
)
|
||||
logger.info(message)
|
||||
send_msg(message)
|
||||
from freqtrade.main import execute_sell
|
||||
execute_sell(trade, current_rate)
|
||||
|
||||
except ValueError:
|
||||
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
|
||||
@@ -288,17 +372,73 @@ def _performance(bot: Bot, update: Update) -> None:
|
||||
.order_by(text('profit_sum DESC')) \
|
||||
.all()
|
||||
|
||||
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
|
||||
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}%</code>'.format(
|
||||
index=i + 1,
|
||||
pair=pair,
|
||||
profit=round(rate, 2)
|
||||
profit=round(rate * 100, 2)
|
||||
) for i, (pair, rate) in enumerate(pair_rates))
|
||||
|
||||
message = '<b>Performance:</b>\n{}\n'.format(stats)
|
||||
message = '<b>Performance:</b>\n{}'.format(stats)
|
||||
logger.debug(message)
|
||||
send_msg(message, parse_mode=ParseMode.HTML)
|
||||
|
||||
|
||||
@authorized_only
|
||||
def _count(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /count.
|
||||
Returns the number of trades running
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
if get_state() != State.RUNNING:
|
||||
send_msg('`trader is not running`', bot=bot)
|
||||
return
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
message = '<b>Count:</b>\ncurrent/max\n{}/{}\n'.format(len(trades), _CONF['max_open_trades'])
|
||||
|
||||
logger.debug(message)
|
||||
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 [table]:* `Lists all open trades`
|
||||
*table :* `will display trades in a table`
|
||||
*/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`
|
||||
*/count:* `Show number of trades running compared to allowed number of trades`
|
||||
*/balance:* `Show account balance per currency`
|
||||
*/help:* `This help message`
|
||||
"""
|
||||
send_msg(message, bot=bot)
|
||||
|
||||
|
||||
def shorten_date(date):
|
||||
"""
|
||||
Trim the date so it fits on small screens
|
||||
"""
|
||||
new_date = re.sub('seconds?', 'sec', date)
|
||||
new_date = re.sub('minutes?', 'min', new_date)
|
||||
new_date = re.sub('hours?', 'h', new_date)
|
||||
new_date = re.sub('days?', 'd', new_date)
|
||||
new_date = re.sub('^an?', '1', new_date)
|
||||
return new_date
|
||||
|
||||
|
||||
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
|
||||
"""
|
||||
Send given markdown message
|
||||
@@ -307,18 +447,17 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
||||
:param parse_mode: telegram parse mode
|
||||
:return: None
|
||||
"""
|
||||
if _CONF['telegram'].get('enabled', False):
|
||||
try:
|
||||
bot = bot or _updater.bot
|
||||
try:
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
except NetworkError as error:
|
||||
# Sometimes the telegram server resets the current connection,
|
||||
# if this is the case we send the message again.
|
||||
logger.warning(
|
||||
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||
error.message
|
||||
)
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
except Exception:
|
||||
logger.exception('Exception occurred within Telegram API')
|
||||
if not is_enabled():
|
||||
return
|
||||
|
||||
bot = bot or _UPDATER.bot
|
||||
try:
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||
except NetworkError as error:
|
||||
# Sometimes the telegram server resets the current connection,
|
||||
# if this is the case we send the message again.
|
||||
logger.warning(
|
||||
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||
error.message
|
||||
)
|
||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
118
freqtrade/tests/conftest.py
Normal file
118
freqtrade/tests/conftest.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
from telegram import Message, Chat, Update
|
||||
|
||||
from freqtrade.misc import CONF_SCHEMA
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def default_conf():
|
||||
""" Returns validated configuration suitable for most tests """
|
||||
configuration = {
|
||||
"max_open_trades": 1,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
"dry_run": True,
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.05,
|
||||
"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": "0"
|
||||
},
|
||||
"initial_state": "running"
|
||||
}
|
||||
validate(configuration, CONF_SCHEMA)
|
||||
return configuration
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def backtest_conf():
|
||||
return {
|
||||
"minimal_roi": {
|
||||
"40": 0.0,
|
||||
"30": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.04
|
||||
},
|
||||
"stoploss": -0.05
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def backdata():
|
||||
result = {}
|
||||
for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']:
|
||||
with open('freqtrade/tests/testdata/' + pair + '.json') as data_file:
|
||||
result[pair] = json.load(data_file)
|
||||
return result
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def update():
|
||||
_update = Update(0)
|
||||
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
|
||||
return _update
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ticker():
|
||||
return MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061,
|
||||
})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def limit_buy_order():
|
||||
return {
|
||||
'id': 'mocked_limit_buy',
|
||||
'type': 'LIMIT_BUY',
|
||||
'pair': 'mocked',
|
||||
'opened': datetime.utcnow(),
|
||||
'rate': 0.07256061,
|
||||
'amount': 206.43811673387373,
|
||||
'remaining': 0.0,
|
||||
'closed': datetime.utcnow(),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def limit_sell_order():
|
||||
return {
|
||||
'id': 'mocked_limit_sell',
|
||||
'type': 'LIMIT_SELL',
|
||||
'pair': 'mocked',
|
||||
'opened': datetime.utcnow(),
|
||||
'rate': 0.0802134,
|
||||
'amount': 206.43811673387373,
|
||||
'remaining': 0.0,
|
||||
'closed': datetime.utcnow(),
|
||||
}
|
39
freqtrade/tests/test_analyze.py
Normal file
39
freqtrade/tests/test_analyze.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from datetime import datetime
|
||||
import json
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||
get_buy_signal
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
|
||||
return parse_ticker_dataframe(json.load(data_file))
|
||||
|
||||
|
||||
def test_dataframe_correct_columns(result):
|
||||
assert result.columns.tolist() == \
|
||||
['close', 'high', 'low', 'open', 'date', 'volume']
|
||||
|
||||
|
||||
def test_dataframe_correct_length(result):
|
||||
assert len(result.index) == 5751
|
||||
|
||||
|
||||
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': datetime.today()}])
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||
assert get_buy_signal('BTC-ETH')
|
||||
|
||||
buydf = DataFrame([{'buy': 0, 'date': datetime.today()}])
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||
assert not get_buy_signal('BTC-ETH')
|
65
freqtrade/tests/test_backtesting.py
Normal file
65
freqtrade/tests/test_backtesting.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.analyze import analyze_ticker
|
||||
from freqtrade.exchange import Bittrex
|
||||
from freqtrade.main import should_sell
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||
|
||||
|
||||
def format_results(results):
|
||||
return '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)
|
||||
|
||||
|
||||
def print_pair_results(pair, results):
|
||||
print('For currency {}:'.format(pair))
|
||||
print(format_results(results[results.currency == pair]))
|
||||
|
||||
|
||||
def backtest(backtest_conf, backdata, mocker):
|
||||
trades = []
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
|
||||
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||
for pair, pair_data in backdata.items():
|
||||
mocked_history.return_value = pair_data
|
||||
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
|
||||
# for each buy point
|
||||
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
||||
trade = Trade(
|
||||
open_rate=row.close,
|
||||
open_date=row.date,
|
||||
amount=1,
|
||||
fee=exchange.get_fee() * 2
|
||||
)
|
||||
# calculate win/lose forwards from buy point
|
||||
for row2 in ticker[row.Index:].itertuples(index=True):
|
||||
if should_sell(trade, row2.close, row2.date):
|
||||
current_profit = trade.calc_profit(row2.close)
|
||||
|
||||
trades.append((pair, current_profit, row2.Index - row.Index))
|
||||
break
|
||||
labels = ['currency', 'profit', 'duration']
|
||||
results = DataFrame.from_records(trades, columns=labels)
|
||||
return results
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||
def test_backtest(backtest_conf, backdata, mocker, report=True):
|
||||
results = backtest(backtest_conf, backdata, mocker)
|
||||
|
||||
print('====================== BACKTESTING REPORT ================================')
|
||||
for pair in backdata:
|
||||
print_pair_results(pair, results)
|
||||
print('TOTAL OVER ALL TRADES:')
|
||||
print(format_results(results))
|
135
freqtrade/tests/test_hyperopt.py
Normal file
135
freqtrade/tests/test_hyperopt.py
Normal file
@@ -0,0 +1,135 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import logging
|
||||
import os
|
||||
from functools import reduce
|
||||
from math import exp
|
||||
from operator import itemgetter
|
||||
|
||||
import pytest
|
||||
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.tests.test_backtesting import backtest, format_results
|
||||
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||
|
||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||
|
||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
||||
TARGET_TRADES = 1200
|
||||
|
||||
|
||||
def buy_strategy_generator(params):
|
||||
print(params)
|
||||
|
||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||
conditions = []
|
||||
# GUARDS AND TRENDS
|
||||
if params['uptrend_long_ema']['enabled']:
|
||||
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||
if params['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['rsi']['enabled']:
|
||||
conditions.append(dataframe['rsi'] < params['rsi']['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),
|
||||
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
||||
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
||||
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
||||
}
|
||||
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(backtest_conf, backdata, mocker):
|
||||
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
|
||||
|
||||
def optimizer(params):
|
||||
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
||||
|
||||
results = backtest(backtest_conf, backdata, mocker)
|
||||
|
||||
result = format_results(results)
|
||||
print(result)
|
||||
|
||||
total_profit = results.profit.sum() * 1000
|
||||
trade_count = len(results.index)
|
||||
|
||||
trade_loss = 1 - 0.8 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5)
|
||||
profit_loss = exp(-total_profit**3 / 10**11)
|
||||
|
||||
return {
|
||||
'loss': trade_loss + profit_loss,
|
||||
'status': STATUS_OK,
|
||||
'result': result
|
||||
}
|
||||
|
||||
space = {
|
||||
'mfi': hp.choice('mfi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)}
|
||||
]),
|
||||
'fastd': hp.choice('fastd', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)}
|
||||
]),
|
||||
'adx': hp.choice('adx', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
|
||||
]),
|
||||
'cci': hp.choice('cci', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
|
||||
]),
|
||||
'rsi': hp.choice('rsi', [
|
||||
{'enabled': False},
|
||||
{'enabled': True, 'value': hp.uniform('rsi-value', 20, 30)}
|
||||
]),
|
||||
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||
{'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'},
|
||||
{'type': 'ao_cross_zero'},
|
||||
{'type': 'ema5_cross_ema10'},
|
||||
{'type': 'macd_cross_signal'},
|
||||
]),
|
||||
}
|
||||
trials = Trials()
|
||||
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=4, trials=trials)
|
||||
print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================')
|
||||
print('Best parameters {}'.format(best))
|
||||
newlist = sorted(trials.results, key=itemgetter('loss'))
|
||||
print('Result: {}'.format(newlist[0]['result']))
|
232
freqtrade/tests/test_main.py
Normal file
232
freqtrade/tests/test_main.py
Normal file
@@ -0,0 +1,232 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import copy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
||||
get_target_bid, _process
|
||||
from freqtrade.misc import get_state, State
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
def test_process_trade_creation(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
init(default_conf, 'sqlite://')
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert len(trades) == 0
|
||||
|
||||
result = _process()
|
||||
assert result is True
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert len(trades) == 1
|
||||
trade = trades[0]
|
||||
assert trade is not None
|
||||
assert trade.stake_amount == default_conf['stake_amount']
|
||||
assert trade.is_open
|
||||
assert trade.open_date is not None
|
||||
assert trade.exchange == Exchanges.BITTREX.name
|
||||
assert trade.open_rate == 0.072661
|
||||
assert trade.amount == 0.6864067381401302
|
||||
|
||||
|
||||
def test_process_exchange_failures(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(side_effect=requests.exceptions.RequestException))
|
||||
init(default_conf, 'sqlite://')
|
||||
result = _process()
|
||||
assert result is False
|
||||
assert sleep_mock.has_calls()
|
||||
|
||||
|
||||
def test_process_runtime_error(default_conf, ticker, mocker):
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(side_effect=RuntimeError))
|
||||
init(default_conf, 'sqlite://')
|
||||
assert get_state() == State.RUNNING
|
||||
|
||||
result = _process()
|
||||
assert result is False
|
||||
assert get_state() == State.STOPPED
|
||||
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
get_order=MagicMock(return_value=limit_buy_order))
|
||||
init(default_conf, 'sqlite://')
|
||||
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert len(trades) == 0
|
||||
result = _process()
|
||||
assert result is True
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
assert len(trades) == 1
|
||||
|
||||
result = _process()
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_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=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
# Save state of current whitelist
|
||||
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||
|
||||
init(default_conf, 'sqlite://')
|
||||
trade = create_trade(15.0)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
assert trade is not None
|
||||
assert trade.stake_amount == 15.0
|
||||
assert trade.is_open
|
||||
assert trade.open_date is not None
|
||||
assert trade.exchange == Exchanges.BITTREX.name
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
assert trade.open_rate == 0.07256061
|
||||
assert trade.amount == 206.43811673387373
|
||||
|
||||
assert whitelist == default_conf['exchange']['pair_whitelist']
|
||||
|
||||
|
||||
def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
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=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
|
||||
with pytest.raises(ValueError, match=r'.*stake amount.*'):
|
||||
create_trade(default_conf['stake_amount'])
|
||||
|
||||
|
||||
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
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=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
with pytest.raises(ValueError, match=r'.*No pair in whitelist.*'):
|
||||
conf = copy.deepcopy(default_conf)
|
||||
conf['exchange']['pair_whitelist'] = []
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
create_trade(default_conf['stake_amount'])
|
||||
|
||||
|
||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
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.17256061,
|
||||
'ask': 0.172661,
|
||||
'last': 0.17256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
sell=MagicMock(return_value='mocked_limit_sell'))
|
||||
init(default_conf, 'sqlite://')
|
||||
trade = create_trade(15.0)
|
||||
trade.update(limit_buy_order)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
assert trade
|
||||
|
||||
handle_trade(trade)
|
||||
assert trade.open_order_id == 'mocked_limit_sell'
|
||||
assert close_trade_if_fulfilled(trade) is False
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
trade.update(limit_sell_order)
|
||||
|
||||
assert trade.close_rate == 0.0802134
|
||||
assert trade.close_profit == 0.10046755
|
||||
assert trade.close_date is not None
|
||||
|
||||
|
||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_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=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||
|
||||
# Create trade and sell it
|
||||
init(default_conf, 'sqlite://')
|
||||
trade = create_trade(15.0)
|
||||
trade.update(limit_buy_order)
|
||||
trade.update(limit_sell_order)
|
||||
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
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
|
||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
||||
handle_trade(trade)
|
||||
|
||||
|
||||
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_bigger_last_ask(mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
66
freqtrade/tests/test_persistence.py
Normal file
66
freqtrade/tests/test_persistence.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import pytest
|
||||
|
||||
from freqtrade.exchange import Exchanges
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
def test_update(limit_buy_order, limit_sell_order):
|
||||
trade = Trade(
|
||||
pair='BTC_ETH',
|
||||
stake_amount=1.00,
|
||||
fee=0.1,
|
||||
exchange=Exchanges.BITTREX,
|
||||
)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate is None
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(limit_buy_order)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate == 0.07256061
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
trade.open_order_id = 'something'
|
||||
trade.update(limit_sell_order)
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate == 0.07256061
|
||||
assert trade.close_profit == 0.00546755
|
||||
assert trade.close_date is not None
|
||||
|
||||
|
||||
def test_update_open_order(limit_buy_order):
|
||||
trade = Trade(
|
||||
pair='BTC_ETH',
|
||||
stake_amount=1.00,
|
||||
fee=0.1,
|
||||
exchange=Exchanges.BITTREX,
|
||||
)
|
||||
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate is None
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
limit_buy_order['closed'] = False
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
assert trade.open_order_id is None
|
||||
assert trade.open_rate is None
|
||||
assert trade.close_profit is None
|
||||
assert trade.close_date is None
|
||||
|
||||
|
||||
def test_update_invalid_order(limit_buy_order):
|
||||
trade = Trade(
|
||||
pair='BTC_ETH',
|
||||
stake_amount=1.00,
|
||||
fee=0.1,
|
||||
exchange=Exchanges.BITTREX,
|
||||
)
|
||||
limit_buy_order['type'] = 'invalid'
|
||||
with pytest.raises(ValueError, match=r'Unknown order type'):
|
||||
trade.update(limit_buy_order)
|
503
freqtrade/tests/test_telegram.py
Normal file
503
freqtrade/tests/test_telegram.py
Normal file
@@ -0,0 +1,503 @@
|
||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors
|
||||
import re
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from telegram import Bot, Update, Message, Chat
|
||||
from telegram.error import NetworkError
|
||||
|
||||
from freqtrade.main import init, create_trade
|
||||
from freqtrade.misc import update_state, State, get_state
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import telegram
|
||||
from freqtrade.rpc.telegram import (
|
||||
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
|
||||
authorized_only, _help, is_enabled, send_msg
|
||||
)
|
||||
|
||||
|
||||
class MagicBot(MagicMock, Bot):
|
||||
pass
|
||||
|
||||
|
||||
def test_is_enabled(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
assert is_enabled() is False
|
||||
|
||||
|
||||
def test_init_disabled(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
|
||||
default_conf['telegram']['enabled'] = False
|
||||
telegram.init(default_conf)
|
||||
|
||||
|
||||
def test_authorized_only(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
|
||||
|
||||
chat = Chat(0, 0)
|
||||
update = Update(randint(1, 100))
|
||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||
state = {'called': False}
|
||||
|
||||
@authorized_only
|
||||
def dummy_handler(*args, **kwargs) -> None:
|
||||
state['called'] = True
|
||||
|
||||
dummy_handler(MagicMock(), update)
|
||||
assert state['called'] is True
|
||||
|
||||
|
||||
def test_authorized_only_unauthorized(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
|
||||
|
||||
chat = Chat(0xdeadbeef, 0)
|
||||
update = Update(randint(1, 100))
|
||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
|
||||
state = {'called': False}
|
||||
|
||||
@authorized_only
|
||||
def dummy_handler(*args, **kwargs) -> None:
|
||||
state['called'] = True
|
||||
|
||||
dummy_handler(MagicMock(), update)
|
||||
assert state['called'] is False
|
||||
|
||||
|
||||
def test_authorized_only_exception(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
|
||||
|
||||
update = Update(randint(1, 100))
|
||||
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
|
||||
|
||||
@authorized_only
|
||||
def dummy_handler(*args, **kwargs) -> None:
|
||||
raise Exception('test')
|
||||
|
||||
dummy_handler(MagicMock(), update)
|
||||
|
||||
|
||||
def test_status_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, 'sqlite://')
|
||||
|
||||
update_state(State.STOPPED)
|
||||
_status(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
update_state(State.RUNNING)
|
||||
_status(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
# Trigger status while we have a fulfilled order for the open trade
|
||||
_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_status_table_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(default_conf, 'sqlite://')
|
||||
update_state(State.STOPPED)
|
||||
_status_table(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
update_state(State.RUNNING)
|
||||
_status_table(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no active order' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_status_table(bot=MagicBot(), update=update)
|
||||
|
||||
text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
|
||||
line = text.split("\n")
|
||||
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
|
||||
|
||||
assert int(fields[0]) == 1
|
||||
assert fields[1] == 'BTC_ETH'
|
||||
assert msg_mock.call_count == 2
|
||||
|
||||
|
||||
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, 'sqlite://')
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||
msg_mock.reset_mock()
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
trade.update(limit_sell_order)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*ROI:* `1.50701325 (10.05%)`' in msg_mock.call_args_list[-1][0][0]
|
||||
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_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.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
|
||||
# Trader is not running
|
||||
update_state(State.STOPPED)
|
||||
update.message.text = '/forcesell 1'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# No argument
|
||||
msg_mock.reset_mock()
|
||||
update_state(State.RUNNING)
|
||||
update.message.text = '/forcesell'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
# Invalid argument
|
||||
msg_mock.reset_mock()
|
||||
update_state(State.RUNNING)
|
||||
update.message.text = '/forcesell 123456'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'no open trade' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker)
|
||||
init(default_conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
|
||||
# Simulate fulfilled LIMIT_BUY order for trade
|
||||
trade.update(limit_buy_order)
|
||||
|
||||
# Simulate fulfilled LIMIT_SELL order for trade
|
||||
trade.update(limit_sell_order)
|
||||
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.is_open = False
|
||||
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 '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
|
||||
def test_count_handle(default_conf, update, ticker, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(default_conf, 'sqlite://')
|
||||
update_state(State.STOPPED)
|
||||
_count(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
msg_mock.reset_mock()
|
||||
update_state(State.RUNNING)
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
trade2 = create_trade(15.0)
|
||||
assert trade
|
||||
assert trade2
|
||||
Trade.session.add(trade)
|
||||
Trade.session.add(trade2)
|
||||
Trade.session.flush()
|
||||
|
||||
_count(bot=MagicBot(), update=update)
|
||||
line = msg_mock.call_args_list[-1][0][0].split("\n")
|
||||
assert line[2] == '{}/{}'.format(2, default_conf['max_open_trades'])
|
||||
|
||||
|
||||
def test_performance_handle_invalid(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
|
||||
# Trader is not running
|
||||
update_state(State.STOPPED)
|
||||
_performance(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_start_handle(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_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_start_handle_already_running(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
update_state(State.RUNNING)
|
||||
assert get_state() == State.RUNNING
|
||||
_start(bot=MagicBot(), update=update)
|
||||
assert get_state() == State.RUNNING
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'already running' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_stop_handle(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_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]
|
||||
|
||||
|
||||
def test_stop_handle_already_stopped(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
init(default_conf, 'sqlite://')
|
||||
update_state(State.STOPPED)
|
||||
assert get_state() == State.STOPPED
|
||||
_stop(bot=MagicBot(), update=update)
|
||||
assert get_state() == State.STOPPED
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_balance_handle(default_conf, update, mocker):
|
||||
mock_balance = [{
|
||||
'Currency': 'BTC',
|
||||
'Balance': 10.0,
|
||||
'Available': 12.0,
|
||||
'Pending': 0.0,
|
||||
'CryptoAddress': 'XXXX',
|
||||
}, {
|
||||
'Currency': 'ETH',
|
||||
'Balance': 0.0,
|
||||
'Available': 0.0,
|
||||
'Pending': 0.0,
|
||||
'CryptoAddress': 'XXXX',
|
||||
}]
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
get_balances=MagicMock(return_value=mock_balance))
|
||||
|
||||
_balance(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'Balance' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_help_handle(default_conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock(),
|
||||
send_msg=msg_mock)
|
||||
|
||||
_help(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 1
|
||||
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_send_msg(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
bot = MagicMock()
|
||||
send_msg('test', bot)
|
||||
assert len(bot.method_calls) == 0
|
||||
bot.reset_mock()
|
||||
|
||||
default_conf['telegram']['enabled'] = True
|
||||
send_msg('test', bot)
|
||||
assert len(bot.method_calls) == 1
|
||||
|
||||
|
||||
def test_send_msg_network_error(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram',
|
||||
_CONF=default_conf,
|
||||
init=MagicMock())
|
||||
default_conf['telegram']['enabled'] = True
|
||||
bot = MagicMock()
|
||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||
with pytest.raises(NetworkError, match=r'Oh snap'):
|
||||
send_msg('test', bot)
|
||||
|
||||
# Bot should've tried to send it twice
|
||||
assert len(bot.method_calls) == 2
|
1
freqtrade/tests/testdata/btc-edg.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-edg.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-etc.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-etc.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-eth.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-eth.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-ltc.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-ltc.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-mtl.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-mtl.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-neo.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-neo.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-omg.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-omg.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pay.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-pay.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pivx.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-pivx.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-qtum.json
vendored
Normal file
1
freqtrade/tests/testdata/btc-qtum.json
vendored
Normal file
File diff suppressed because one or more lines are too long
24
freqtrade/tests/testdata/download_backtest_data.py
vendored
Executable file
24
freqtrade/tests/testdata/download_backtest_data.py
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""This script generate json data from bittrex"""
|
||||
import json
|
||||
from os import path
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.exchange import Bittrex
|
||||
|
||||
PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT']
|
||||
TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5)
|
||||
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
||||
|
||||
# Init Bittrex exchange
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
|
||||
for pair in PAIRS:
|
||||
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
||||
filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format(
|
||||
pair.lower(),
|
||||
TICKER_INTERVAL,
|
||||
))
|
||||
with open(filename, 'w') as fp:
|
||||
json.dump(data, fp)
|
0
freqtrade/vendor/__init__.py
vendored
Normal file
0
freqtrade/vendor/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
625
freqtrade/vendor/qtpylib/indicators.py
vendored
Normal file
625
freqtrade/vendor/qtpylib/indicators.py
vendored
Normal file
@@ -0,0 +1,625 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# QTPyLib: Quantitative Trading Python Library
|
||||
# https://github.com/ranaroussi/qtpylib
|
||||
#
|
||||
# Copyright 2016 Ran Aroussi
|
||||
#
|
||||
# Licensed under the GNU Lesser General Public License, v3.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.gnu.org/licenses/lgpl-3.0.en.html
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import warnings
|
||||
import sys
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pandas.core.base import PandasObject
|
||||
|
||||
# =============================================
|
||||
# check min, python version
|
||||
if sys.version_info < (3, 4):
|
||||
raise SystemError("QTPyLib requires Python version >= 3.4")
|
||||
|
||||
# =============================================
|
||||
warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
||||
|
||||
# =============================================
|
||||
|
||||
|
||||
def numpy_rolling_window(data, window):
|
||||
shape = data.shape[:-1] + (data.shape[-1] - window + 1, window)
|
||||
strides = data.strides + (data.strides[-1],)
|
||||
return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides)
|
||||
|
||||
|
||||
def numpy_rolling_series(func):
|
||||
def func_wrapper(data, window, as_source=False):
|
||||
series = data.values if isinstance(data, pd.Series) else data
|
||||
|
||||
new_series = np.empty(len(series)) * np.nan
|
||||
calculated = func(series, window)
|
||||
new_series[-len(calculated):] = calculated
|
||||
|
||||
if as_source and isinstance(data, pd.Series):
|
||||
return pd.Series(index=data.index, data=new_series)
|
||||
|
||||
return new_series
|
||||
|
||||
return func_wrapper
|
||||
|
||||
|
||||
@numpy_rolling_series
|
||||
def numpy_rolling_mean(data, window, as_source=False):
|
||||
return np.mean(numpy_rolling_window(data, window), -1)
|
||||
|
||||
|
||||
@numpy_rolling_series
|
||||
def numpy_rolling_std(data, window, as_source=False):
|
||||
return np.std(numpy_rolling_window(data, window), -1)
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
|
||||
def session(df, start='17:00', end='16:00'):
|
||||
""" remove previous globex day from df """
|
||||
if len(df) == 0:
|
||||
return df
|
||||
|
||||
# get start/end/now as decimals
|
||||
int_start = list(map(int, start.split(':')))
|
||||
int_start = (int_start[0] + int_start[1] - 1 / 100) - 0.0001
|
||||
int_end = list(map(int, end.split(':')))
|
||||
int_end = int_end[0] + int_end[1] / 100
|
||||
int_now = (df[-1:].index.hour[0] + (df[:1].index.minute[0]) / 100)
|
||||
|
||||
# same-dat session?
|
||||
is_same_day = int_end > int_start
|
||||
|
||||
# set pointers
|
||||
curr = prev = df[-1:].index[0].strftime('%Y-%m-%d')
|
||||
|
||||
# globex/forex session
|
||||
if not is_same_day:
|
||||
prev = (datetime.strptime(curr, '%Y-%m-%d') -
|
||||
timedelta(1)).strftime('%Y-%m-%d')
|
||||
|
||||
# slice
|
||||
if int_now >= int_start:
|
||||
df = df[df.index >= curr + ' ' + start]
|
||||
else:
|
||||
df = df[df.index >= prev + ' ' + start]
|
||||
|
||||
return df.copy()
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def heikinashi(bars):
|
||||
bars = bars.copy()
|
||||
bars['ha_close'] = (bars['open'] + bars['high'] +
|
||||
bars['low'] + bars['close']) / 4
|
||||
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
|
||||
bars.loc[:1, 'ha_open'] = bars['open'].values[0]
|
||||
bars.loc[1:, 'ha_open'] = (
|
||||
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
|
||||
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
|
||||
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
|
||||
|
||||
return pd.DataFrame(
|
||||
index=bars.index,
|
||||
data={
|
||||
'open': bars['ha_open'],
|
||||
'high': bars['ha_high'],
|
||||
'low': bars['ha_low'],
|
||||
'close': bars['ha_close']})
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2,
|
||||
rsi_signal_len=7, bollinger_std=1.6185):
|
||||
rsi_series = rsi(series, rsi_len)
|
||||
bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
|
||||
signal = sma(rsi_series, rsi_signal_len)
|
||||
rsi_series = sma(rsi_series, rsi_smoothing)
|
||||
|
||||
return pd.DataFrame(index=series.index, data={
|
||||
"rsi": rsi_series,
|
||||
"signal": signal,
|
||||
"bbupper": bb_series['upper'],
|
||||
"bblower": bb_series['lower'],
|
||||
"bbmid": bb_series['mid']
|
||||
})
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
|
||||
def awesome_oscillator(df, weighted=False, fast=5, slow=34):
|
||||
midprice = (df['high'] + df['low']) / 2
|
||||
|
||||
if weighted:
|
||||
ao = (midprice.ewm(fast).mean() - midprice.ewm(slow).mean()).values
|
||||
else:
|
||||
ao = numpy_rolling_mean(midprice, fast) - \
|
||||
numpy_rolling_mean(midprice, slow)
|
||||
|
||||
return pd.Series(index=df.index, data=ao)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def nans(len=1):
|
||||
mtx = np.empty(len)
|
||||
mtx[:] = np.nan
|
||||
return mtx
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def typical_price(bars):
|
||||
res = (bars['high'] + bars['low'] + bars['close']) / 3.
|
||||
return pd.Series(index=bars.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def mid_price(bars):
|
||||
res = (bars['high'] + bars['low']) / 2.
|
||||
return pd.Series(index=bars.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def ibs(bars):
|
||||
""" Internal bar strength """
|
||||
res = np.round((bars['close'] - bars['low']) /
|
||||
(bars['high'] - bars['low']), 2)
|
||||
return pd.Series(index=bars.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def true_range(bars):
|
||||
return pd.DataFrame({
|
||||
"hl": bars['high'] - bars['low'],
|
||||
"hc": abs(bars['high'] - bars['close'].shift(1)),
|
||||
"lc": abs(bars['low'] - bars['close'].shift(1))
|
||||
}).max(axis=1)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def atr(bars, window=14, exp=False):
|
||||
tr = true_range(bars)
|
||||
|
||||
if exp:
|
||||
res = rolling_weighted_mean(tr, window)
|
||||
else:
|
||||
res = rolling_mean(tr, window)
|
||||
|
||||
res = pd.Series(res)
|
||||
return (res.shift(1) * (window - 1) + res) / window
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def crossed(series1, series2, direction=None):
|
||||
if isinstance(series1, np.ndarray):
|
||||
series1 = pd.Series(series1)
|
||||
|
||||
if isinstance(series2, int) or isinstance(series2, float) or isinstance(series2, np.ndarray):
|
||||
series2 = pd.Series(index=series1.index, data=series2)
|
||||
|
||||
if direction is None or direction == "above":
|
||||
above = pd.Series((series1 > series2) & (
|
||||
series1.shift(1) <= series2.shift(1)))
|
||||
|
||||
if direction is None or direction == "below":
|
||||
below = pd.Series((series1 < series2) & (
|
||||
series1.shift(1) >= series2.shift(1)))
|
||||
|
||||
if direction is None:
|
||||
return above or below
|
||||
|
||||
return above if direction is "above" else below
|
||||
|
||||
|
||||
def crossed_above(series1, series2):
|
||||
return crossed(series1, series2, "above")
|
||||
|
||||
|
||||
def crossed_below(series1, series2):
|
||||
return crossed(series1, series2, "below")
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
|
||||
def rolling_std(series, window=200, min_periods=None):
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
try:
|
||||
if min_periods == window:
|
||||
return numpy_rolling_std(series, window, True)
|
||||
else:
|
||||
try:
|
||||
return series.rolling(window=window, min_periods=min_periods).std()
|
||||
except BaseException:
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
|
||||
except BaseException:
|
||||
return pd.rolling_std(series, window=window, min_periods=min_periods)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def rolling_mean(series, window=200, min_periods=None):
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
try:
|
||||
if min_periods == window:
|
||||
return numpy_rolling_mean(series, window, True)
|
||||
else:
|
||||
try:
|
||||
return series.rolling(window=window, min_periods=min_periods).mean()
|
||||
except BaseException:
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
|
||||
except BaseException:
|
||||
return pd.rolling_mean(series, window=window, min_periods=min_periods)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def rolling_min(series, window=14, min_periods=None):
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
try:
|
||||
try:
|
||||
return series.rolling(window=window, min_periods=min_periods).min()
|
||||
except BaseException:
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||
except BaseException:
|
||||
return pd.rolling_min(series, window=window, min_periods=min_periods)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def rolling_max(series, window=14, min_periods=None):
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
try:
|
||||
try:
|
||||
return series.rolling(window=window, min_periods=min_periods).min()
|
||||
except BaseException:
|
||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||
except BaseException:
|
||||
return pd.rolling_min(series, window=window, min_periods=min_periods)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def rolling_weighted_mean(series, window=200, min_periods=None):
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
try:
|
||||
return series.ewm(span=window, min_periods=min_periods).mean()
|
||||
except BaseException:
|
||||
return pd.ewma(series, span=window, min_periods=min_periods)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def hull_moving_average(series, window=200):
|
||||
wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \
|
||||
rolling_weighted_mean(series, window=window)
|
||||
return rolling_weighted_mean(wma, window=np.sqrt(window))
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def sma(series, window=200, min_periods=None):
|
||||
return rolling_mean(series, window=window, min_periods=min_periods)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def wma(series, window=200, min_periods=None):
|
||||
return rolling_weighted_mean(series, window=window, min_periods=min_periods)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def hma(series, window=200):
|
||||
return hull_moving_average(series, window=window)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def vwap(bars):
|
||||
"""
|
||||
calculate vwap of entire time series
|
||||
(input can be pandas series or numpy array)
|
||||
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
|
||||
"""
|
||||
typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
|
||||
volume = bars['volume'].values
|
||||
|
||||
return pd.Series(index=bars.index,
|
||||
data=np.cumsum(volume * typical) / np.cumsum(volume))
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def rolling_vwap(bars, window=200, min_periods=None):
|
||||
"""
|
||||
calculate vwap using moving window
|
||||
(input can be pandas series or numpy array)
|
||||
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
|
||||
"""
|
||||
min_periods = window if min_periods is None else min_periods
|
||||
|
||||
typical = ((bars['high'] + bars['low'] + bars['close']) / 3)
|
||||
volume = bars['volume']
|
||||
|
||||
left = (volume * typical).rolling(window=window,
|
||||
min_periods=min_periods).sum()
|
||||
right = volume.rolling(window=window, min_periods=min_periods).sum()
|
||||
|
||||
return pd.Series(index=bars.index, data=(left / right))
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def rsi(series, window=14):
|
||||
"""
|
||||
compute the n period relative strength indicator
|
||||
"""
|
||||
# 100-(100/relative_strength)
|
||||
deltas = np.diff(series)
|
||||
seed = deltas[:window + 1]
|
||||
|
||||
# default values
|
||||
ups = seed[seed > 0].sum() / window
|
||||
downs = -seed[seed < 0].sum() / window
|
||||
rsival = np.zeros_like(series)
|
||||
rsival[:window] = 100. - 100. / (1. + ups / downs)
|
||||
|
||||
# period values
|
||||
for i in range(window, len(series)):
|
||||
delta = deltas[i - 1]
|
||||
if delta > 0:
|
||||
upval = delta
|
||||
downval = 0
|
||||
else:
|
||||
upval = 0
|
||||
downval = -delta
|
||||
|
||||
ups = (ups * (window - 1) + upval) / window
|
||||
downs = (downs * (window - 1.) + downval) / window
|
||||
rsival[i] = 100. - 100. / (1. + ups / downs)
|
||||
|
||||
# return rsival
|
||||
return pd.Series(index=series.index, data=rsival)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def macd(series, fast=3, slow=10, smooth=16):
|
||||
"""
|
||||
compute the MACD (Moving Average Convergence/Divergence)
|
||||
using a fast and slow exponential moving avg'
|
||||
return value is emaslow, emafast, macd which are len(x) arrays
|
||||
"""
|
||||
macd = rolling_weighted_mean(series, window=fast) - \
|
||||
rolling_weighted_mean(series, window=slow)
|
||||
signal = rolling_weighted_mean(macd, window=smooth)
|
||||
histogram = macd - signal
|
||||
# return macd, signal, histogram
|
||||
return pd.DataFrame(index=series.index, data={
|
||||
'macd': macd.values,
|
||||
'signal': signal.values,
|
||||
'histogram': histogram.values
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def bollinger_bands(series, window=20, stds=2):
|
||||
sma = rolling_mean(series, window=window)
|
||||
std = rolling_std(series, window=window)
|
||||
upper = sma + std * stds
|
||||
lower = sma - std * stds
|
||||
|
||||
return pd.DataFrame(index=series.index, data={
|
||||
'upper': upper,
|
||||
'mid': sma,
|
||||
'lower': lower
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def weighted_bollinger_bands(series, window=20, stds=2):
|
||||
ema = rolling_weighted_mean(series, window=window)
|
||||
std = rolling_std(series, window=window)
|
||||
upper = ema + std * stds
|
||||
lower = ema - std * stds
|
||||
|
||||
return pd.DataFrame(index=series.index, data={
|
||||
'upper': upper.values,
|
||||
'mid': ema.values,
|
||||
'lower': lower.values
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def returns(series):
|
||||
try:
|
||||
res = (series / series.shift(1) -
|
||||
1).replace([np.inf, -np.inf], float('NaN'))
|
||||
except BaseException:
|
||||
res = nans(len(series))
|
||||
|
||||
return pd.Series(index=series.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def log_returns(series):
|
||||
try:
|
||||
res = np.log(series / series.shift(1)
|
||||
).replace([np.inf, -np.inf], float('NaN'))
|
||||
except BaseException:
|
||||
res = nans(len(series))
|
||||
|
||||
return pd.Series(index=series.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def implied_volatility(series, window=252):
|
||||
try:
|
||||
logret = np.log(series / series.shift(1)
|
||||
).replace([np.inf, -np.inf], float('NaN'))
|
||||
res = numpy_rolling_std(logret, window) * np.sqrt(window)
|
||||
except BaseException:
|
||||
res = nans(len(series))
|
||||
|
||||
return pd.Series(index=series.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def keltner_channel(bars, window=14, atrs=2):
|
||||
typical_mean = rolling_mean(typical_price(bars), window)
|
||||
atrval = atr(bars, window) * atrs
|
||||
|
||||
upper = typical_mean + atrval
|
||||
lower = typical_mean - atrval
|
||||
|
||||
return pd.DataFrame(index=bars.index, data={
|
||||
'upper': upper.values,
|
||||
'mid': typical_mean.values,
|
||||
'lower': lower.values
|
||||
})
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def roc(series, window=14):
|
||||
"""
|
||||
compute rate of change
|
||||
"""
|
||||
res = (series - series.shift(window)) / series.shift(window)
|
||||
return pd.Series(index=series.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def cci(series, window=14):
|
||||
"""
|
||||
compute commodity channel index
|
||||
"""
|
||||
price = typical_price(series)
|
||||
typical_mean = rolling_mean(price, window)
|
||||
res = (price - typical_mean) / (.015 * np.std(typical_mean))
|
||||
return pd.Series(index=series.index, data=res)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def stoch(df, window=14, d=3, k=3, fast=False):
|
||||
"""
|
||||
compute the n period relative strength indicator
|
||||
http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html
|
||||
"""
|
||||
highs_ma = pd.concat([df['high'].shift(i)
|
||||
for i in np.arange(window)], 1).apply(list, 1)
|
||||
highs_ma = highs_ma.T.max().T
|
||||
|
||||
lows_ma = pd.concat([df['low'].shift(i)
|
||||
for i in np.arange(window)], 1).apply(list, 1)
|
||||
lows_ma = lows_ma.T.min().T
|
||||
|
||||
fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100
|
||||
fast_d = numpy_rolling_mean(fast_k, d)
|
||||
|
||||
if fast:
|
||||
data = {
|
||||
'k': fast_k,
|
||||
'd': fast_d
|
||||
}
|
||||
|
||||
else:
|
||||
slow_k = numpy_rolling_mean(fast_k, k)
|
||||
slow_d = numpy_rolling_mean(slow_k, d)
|
||||
data = {
|
||||
'k': slow_k,
|
||||
'd': slow_d
|
||||
}
|
||||
|
||||
return pd.DataFrame(index=df.index, data=data)
|
||||
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
def zscore(bars, window=20, stds=1, col='close'):
|
||||
""" get zscore of price """
|
||||
std = numpy_rolling_std(bars[col], window)
|
||||
mean = numpy_rolling_mean(bars[col], window)
|
||||
return (bars[col] - mean) / (std * stds)
|
||||
|
||||
# ---------------------------------------------
|
||||
|
||||
|
||||
def pvt(bars):
|
||||
""" Price Volume Trend """
|
||||
pvt = ((bars['close'] - bars['close'].shift(1)) /
|
||||
bars['close'].shift(1)) * bars['volume']
|
||||
return pvt.cumsum()
|
||||
|
||||
|
||||
# =============================================
|
||||
|
||||
PandasObject.session = session
|
||||
PandasObject.atr = atr
|
||||
PandasObject.bollinger_bands = bollinger_bands
|
||||
PandasObject.cci = cci
|
||||
PandasObject.crossed = crossed
|
||||
PandasObject.crossed_above = crossed_above
|
||||
PandasObject.crossed_below = crossed_below
|
||||
PandasObject.heikinashi = heikinashi
|
||||
PandasObject.hull_moving_average = hull_moving_average
|
||||
PandasObject.ibs = ibs
|
||||
PandasObject.implied_volatility = implied_volatility
|
||||
PandasObject.keltner_channel = keltner_channel
|
||||
PandasObject.log_returns = log_returns
|
||||
PandasObject.macd = macd
|
||||
PandasObject.returns = returns
|
||||
PandasObject.roc = roc
|
||||
PandasObject.rolling_max = rolling_max
|
||||
PandasObject.rolling_min = rolling_min
|
||||
PandasObject.rolling_mean = rolling_mean
|
||||
PandasObject.rolling_std = rolling_std
|
||||
PandasObject.rsi = rsi
|
||||
PandasObject.stoch = stoch
|
||||
PandasObject.zscore = zscore
|
||||
PandasObject.pvt = pvt
|
||||
PandasObject.tdi = tdi
|
||||
PandasObject.true_range = true_range
|
||||
PandasObject.mid_price = mid_price
|
||||
PandasObject.typical_price = typical_price
|
||||
PandasObject.vwap = vwap
|
||||
PandasObject.rolling_vwap = rolling_vwap
|
||||
PandasObject.weighted_bollinger_bands = weighted_bollinger_bands
|
||||
PandasObject.rolling_weighted_mean = rolling_weighted_mean
|
||||
|
||||
PandasObject.sma = sma
|
||||
PandasObject.wma = wma
|
||||
PandasObject.hma = hma
|
@@ -1,89 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm.scoping import scoped_session
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
|
||||
from sqlalchemy.types import Enum
|
||||
|
||||
import exchange
|
||||
|
||||
|
||||
_CONF = {}
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
registers all known command handlers
|
||||
and starts polling for message updates
|
||||
:param config: config to use
|
||||
:param db_url: database connector string for sqlalchemy (Optional)
|
||||
:return: None
|
||||
"""
|
||||
_CONF.update(config)
|
||||
if not db_url:
|
||||
if _CONF.get('dry_run', False):
|
||||
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
|
||||
else:
|
||||
db_url = 'sqlite:///tradesv2.sqlite'
|
||||
|
||||
engine = create_engine(db_url, echo=False)
|
||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||
Trade.session = session()
|
||||
Trade.query = session.query_property()
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
class Trade(Base):
|
||||
__tablename__ = 'trades'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
exchange = Column(Enum(exchange.Exchange), nullable=False)
|
||||
pair = Column(String, nullable=False)
|
||||
is_open = Column(Boolean, nullable=False, default=True)
|
||||
open_rate = Column(Float, nullable=False)
|
||||
close_rate = Column(Float)
|
||||
close_profit = Column(Float)
|
||||
stake_amount = Column(Float, name='btc_amount', nullable=False)
|
||||
amount = Column(Float, nullable=False)
|
||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
close_date = Column(DateTime)
|
||||
open_order_id = Column(String)
|
||||
|
||||
def __repr__(self):
|
||||
if self.is_open:
|
||||
open_since = 'closed'
|
||||
else:
|
||||
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
|
||||
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
|
||||
self.id,
|
||||
self.pair,
|
||||
self.amount,
|
||||
self.open_rate,
|
||||
open_since
|
||||
)
|
||||
|
||||
def exec_sell_order(self, rate: float, amount: float) -> float:
|
||||
"""
|
||||
Executes a sell for the given trade and updated the entity.
|
||||
:param rate: rate to sell for
|
||||
:param amount: amount to sell
|
||||
:return: current profit as percentage
|
||||
"""
|
||||
profit = 100 * ((rate - self.open_rate) / self.open_rate)
|
||||
|
||||
# Execute sell and update trade record
|
||||
order_id = exchange.sell(str(self.pair), rate, amount)
|
||||
self.close_rate = rate
|
||||
self.close_profit = profit
|
||||
self.close_date = datetime.utcnow()
|
||||
self.open_order_id = order_id
|
||||
|
||||
# Flush changes
|
||||
Trade.session.flush()
|
||||
return profit
|
@@ -1,14 +1,24 @@
|
||||
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
|
||||
SQLAlchemy==1.1.13
|
||||
python-telegram-bot==7.0.1
|
||||
-e git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex
|
||||
SQLAlchemy==1.1.14
|
||||
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
|
||||
matplotlib==2.0.2
|
||||
scikit-learn==0.19.0
|
||||
scipy==0.19.1
|
||||
jsonschema==2.6.0
|
||||
numpy==1.13.3
|
||||
TA-Lib==0.4.10
|
||||
#PYQT5==5.9
|
||||
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
|
||||
tabulate==0.8.1
|
||||
|
||||
# Required for plotting data
|
||||
#matplotlib==2.1.0
|
||||
#PYQT5==5.9
|
||||
|
48
setup.py
Normal file
48
setup.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from sys import version_info
|
||||
from setuptools import setup
|
||||
|
||||
if version_info.major == 3 and version_info.minor < 6 or \
|
||||
version_info.major < 3:
|
||||
print('Your Python interpreter must be 3.6 or greater!')
|
||||
exit(1)
|
||||
|
||||
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',
|
||||
'SQLAlchemy',
|
||||
'python-telegram-bot',
|
||||
'arrow',
|
||||
'requests',
|
||||
'urllib3',
|
||||
'wrapt',
|
||||
'pandas',
|
||||
'scikit-learn',
|
||||
'scipy',
|
||||
'jsonschema',
|
||||
'TA-Lib',
|
||||
'tabulate',
|
||||
],
|
||||
dependency_links=[
|
||||
"git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex"
|
||||
],
|
||||
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()
|
1
test/testdata/btc-edg.json
vendored
1
test/testdata/btc-edg.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-etc.json
vendored
1
test/testdata/btc-etc.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-eth.json
vendored
1
test/testdata/btc-eth.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-ltc.json
vendored
1
test/testdata/btc-ltc.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-mtl.json
vendored
1
test/testdata/btc-mtl.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-neo.json
vendored
1
test/testdata/btc-neo.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-omg.json
vendored
1
test/testdata/btc-omg.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-pay.json
vendored
1
test/testdata/btc-pay.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-pivx.json
vendored
1
test/testdata/btc-pivx.json
vendored
File diff suppressed because one or more lines are too long
1
test/testdata/btc-qtum.json
vendored
1
test/testdata/btc-qtum.json
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user