Merge aef42336e6
into c20030783b
This commit is contained in:
commit
5ef27fa048
2
.coveragerc
Normal file
2
.coveragerc
Normal file
@ -0,0 +1,2 @@
|
||||
[run]
|
||||
omit = freqtrade/tests/*
|
@ -4,10 +4,6 @@ os:
|
||||
language: python
|
||||
python:
|
||||
- 3.6
|
||||
- nightly
|
||||
matrix:
|
||||
allow_failures:
|
||||
- python: nightly
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
@ -19,9 +15,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=
|
||||
|
17
Dockerfile
17
Dockerfile
@ -1,17 +1,20 @@
|
||||
FROM python:3.6.2
|
||||
|
||||
RUN pip install numpy
|
||||
RUN apt-get update
|
||||
RUN apt-get -y install build-essential
|
||||
RUN apt-get -y install build-essential
|
||||
|
||||
# Install TA-lib
|
||||
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
|
||||
ENV LD_LIBRARY_PATH /usr/local/lib
|
||||
|
||||
RUN mkdir -p /freqtrade
|
||||
# Prepare environment
|
||||
RUN mkdir /freqtrade
|
||||
COPY . /freqtrade/
|
||||
WORKDIR /freqtrade
|
||||
|
||||
ADD ./requirements.txt /freqtrade/requirements.txt
|
||||
RUN pip install -r requirements.txt
|
||||
ADD . /freqtrade
|
||||
CMD python main.py
|
||||
# Install dependencies and execute
|
||||
RUN pip install -r requirements.txt
|
||||
RUN pip install -e .
|
||||
CMD ["freqtrade"]
|
||||
|
6
MANIFEST.in
Normal file
6
MANIFEST.in
Normal file
@ -0,0 +1,6 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include config.json.example
|
||||
include freqtrade/rpc/*.py
|
||||
include freqtrade/tests/*.py
|
||||
include freqtrade/tests/testdata/*.json
|
12
README.md
12
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.
|
||||
@ -63,7 +65,8 @@ $ 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)).
|
||||
@ -71,7 +74,12 @@ There is also an [article](https://www.sales4k.com/blockchain/high-frequency-tra
|
||||
#### Execute tests
|
||||
|
||||
```
|
||||
$ python -m unittest
|
||||
$ pytest
|
||||
```
|
||||
This will by default skip the slow running backtest set. To run backtest set:
|
||||
|
||||
```
|
||||
$ BACKTEST=true pytest
|
||||
```
|
||||
|
||||
#### Docker
|
||||
|
4
bin/freqtrade
Executable file
4
bin/freqtrade
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from freqtrade.main import main
|
||||
main()
|
@ -4,11 +4,12 @@
|
||||
"stake_amount": 0.05,
|
||||
"dry_run": false,
|
||||
"minimal_roi": {
|
||||
"2880": 0.005,
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
"60": 0.0,
|
||||
"40": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.03
|
||||
},
|
||||
"stoploss": -0.10,
|
||||
"stoploss": -0.40,
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
|
3
freqtrade/__init__.py
Normal file
3
freqtrade/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
__version__ = '0.10.0'
|
||||
|
||||
from . import main
|
@ -43,15 +43,20 @@ def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame
|
||||
.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)
|
||||
|
||||
stoch = ta.STOCHF(dataframe)
|
||||
dataframe['fastd'] = stoch['fastd']
|
||||
dataframe['fastk'] = stoch['fastk']
|
||||
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
|
||||
dataframe['cci'] = ta.CCI(dataframe, timeperiod=5)
|
||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=100)
|
||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=4)
|
||||
dataframe['mfi'] = ta.MFI(dataframe)
|
||||
return dataframe
|
||||
|
||||
|
||||
@ -61,26 +66,14 @@ def populate_buy_trend(dataframe: DataFrame) -> 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
|
||||
(dataframe['close'] < dataframe['sma']) &
|
||||
(dataframe['cci'] < -100) &
|
||||
(dataframe['tema'] <= dataframe['blower']) &
|
||||
(dataframe['mfi'] < 30) &
|
||||
(dataframe['fastd'] < 20) &
|
||||
(dataframe['adx'] > 20),
|
||||
'buy'] = 1
|
||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||
|
||||
@ -93,9 +86,14 @@ def analyze_ticker(pair: str) -> DataFrame:
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
minimum_date = arrow.utcnow().shift(hours=-6)
|
||||
minimum_date = arrow.utcnow().shift(hours=-24)
|
||||
data = get_ticker(pair, minimum_date)
|
||||
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return dataframe
|
||||
|
||||
dataframe = populate_indicators(dataframe)
|
||||
dataframe = populate_buy_trend(dataframe)
|
||||
return dataframe
|
||||
@ -107,6 +105,10 @@ def get_buy_signal(pair: str) -> bool:
|
||||
: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
|
||||
@ -138,12 +140,13 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
||||
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.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
|
||||
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, [25] * len(dataframe.index.values))
|
||||
# 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()
|
||||
|
||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||
@ -156,7 +159,7 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
||||
if __name__ == '__main__':
|
||||
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||
while True:
|
||||
test_pair = 'BTC_ANT'
|
||||
test_pair = 'BTC_ETH'
|
||||
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||
# get_buy_signal(pair)
|
||||
plot_dataframe(analyze_ticker(test_pair), test_pair)
|
@ -39,11 +39,20 @@ def init(config: dict) -> None:
|
||||
raise RuntimeError('No exchange specified. Aborting!')
|
||||
|
||||
# Check if all pairs are available
|
||||
validate_pairs(config[EXCHANGE.name.lower()]['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 = get_markets()
|
||||
exchange_name = EXCHANGE.name.lower()
|
||||
for pair in config[exchange_name]['pair_whitelist']:
|
||||
for pair in pairs:
|
||||
if pair not in markets:
|
||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name))
|
||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower()))
|
||||
|
||||
|
||||
def buy(pair: str, rate: float, amount: float) -> str:
|
@ -6,24 +6,19 @@ import traceback
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
import copy
|
||||
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 exchange, persistence, __version__
|
||||
from freqtrade.analyze import get_buy_signal
|
||||
from freqtrade.misc import State, update_state, get_state, CONF_SCHEMA
|
||||
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 = {}
|
||||
|
||||
|
||||
@ -94,10 +89,7 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
|
||||
# Get available balance
|
||||
currency = trade.pair.split('_')[1]
|
||||
balance = exchange.get_balance(currency)
|
||||
whitelist = _CONF[trade.exchange.name.lower()]['pair_whitelist']
|
||||
|
||||
profit = trade.exec_sell_order(current_rate, balance)
|
||||
whitelist.append(trade.pair)
|
||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||
trade.exchange.name,
|
||||
trade.pair.replace('_', '/'),
|
||||
@ -166,7 +158,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
||||
: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.name.lower()]['pair_whitelist'])
|
||||
# Check if stake_amount is fulfilled
|
||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||
raise ValueError(
|
||||
@ -174,11 +166,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
||||
)
|
||||
|
||||
# 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)
|
||||
@ -238,7 +226,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
|
||||
def app(config: dict) -> None:
|
||||
"""
|
||||
Main function which handles the application state
|
||||
Main loop which handles the application state
|
||||
:param config: config as dict
|
||||
:return: None
|
||||
"""
|
||||
@ -269,8 +257,17 @@ def app(config: dict) -> None:
|
||||
telegram.send_msg('*Status:* `Trader has stopped`')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main():
|
||||
"""
|
||||
Loads and validates the config and starts the main loop
|
||||
:return: None
|
||||
"""
|
||||
global _CONF
|
||||
with open('config.json') as file:
|
||||
_CONF = json.load(file)
|
||||
validate(_CONF, CONF_SCHEMA)
|
||||
app(_CONF)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -5,11 +5,9 @@ from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create
|
||||
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
|
||||
|
||||
from freqtrade import exchange
|
||||
|
||||
_CONF = {}
|
||||
|
@ -4,14 +4,13 @@ from typing import Callable, Any
|
||||
|
||||
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)
|
||||
@ -31,9 +30,12 @@ def init(config: dict) -> None:
|
||||
:return: None
|
||||
"""
|
||||
global _updater
|
||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||
|
||||
_CONF.update(config)
|
||||
if not _CONF['telegram']['enabled']:
|
||||
return
|
||||
|
||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||
|
||||
# Register command handler and start telegram message polling
|
||||
handles = [
|
47
freqtrade/tests/test_analyze.py
Normal file
47
freqtrade/tests/test_analyze.py
Normal file
@ -0,0 +1,47 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import pytest
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||
get_buy_signal
|
||||
|
||||
RESULT_BITTREX = {
|
||||
'success': True,
|
||||
'message': '',
|
||||
'result': [
|
||||
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 22.17210568, 'T': '2017-08-30T10:40:00', 'BV': 0.01448082},
|
||||
{'O': 0.00066194, 'H': 0.00066195, 'L': 0.00066194, 'C': 0.00066195, 'V': 33.4727437, 'T': '2017-08-30T10:34:00', 'BV': 0.02215696},
|
||||
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 53.85127609, 'T': '2017-08-30T10:37:00', 'BV': 0.0351708},
|
||||
{'O': 0.00066194, 'H': 0.00066194, 'L': 0.00065311, 'C': 0.00065311, 'V': 46.29210665, 'T': '2017-08-30T10:42:00', 'BV': 0.03063118},
|
||||
]
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def result():
|
||||
return parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
|
||||
|
||||
def test_dataframe_has_correct_columns(result):
|
||||
assert result.columns.tolist() == \
|
||||
['close', 'high', 'low', 'open', 'date', 'volume']
|
||||
|
||||
def test_orders_by_date(result):
|
||||
assert result['date'].tolist() == \
|
||||
['2017-08-30T10:34:00',
|
||||
'2017-08-30T10:37:00',
|
||||
'2017-08-30T10:40:00',
|
||||
'2017-08-30T10:42:00']
|
||||
|
||||
def test_populates_buy_trend(result):
|
||||
dataframe = populate_buy_trend(populate_indicators(result))
|
||||
assert 'buy' in dataframe.columns
|
||||
assert 'buy_price' in dataframe.columns
|
||||
|
||||
def test_returns_latest_buy_signal(mocker):
|
||||
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||
assert get_buy_signal('BTC-ETH')
|
||||
|
||||
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||
assert not get_buy_signal('BTC-ETH')
|
77
freqtrade/tests/test_backtesting.py
Normal file
77
freqtrade/tests/test_backtesting.py
Normal file
@ -0,0 +1,77 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
import pytest
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.analyze import analyze_ticker
|
||||
from freqtrade.main import should_sell
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||
|
||||
def print_results(results):
|
||||
print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
||||
len(results.index),
|
||||
results.profit.mean() * 100.0,
|
||||
results.profit.sum(),
|
||||
results.duration.mean()*5
|
||||
))
|
||||
|
||||
@pytest.fixture
|
||||
def pairs():
|
||||
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
return {
|
||||
"minimal_roi": {
|
||||
"60": 0.0,
|
||||
"40": 0.01,
|
||||
"20": 0.02,
|
||||
"0": 0.03
|
||||
},
|
||||
"stoploss": -0.40
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||
def test_backtest(conf, pairs, mocker):
|
||||
trades = []
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
for pair in pairs:
|
||||
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
||||
data = json.load(data_file)
|
||||
|
||||
mocker.patch('freqtrade.analyze.get_ticker', return_value=data)
|
||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||
ticker = analyze_ticker(pair)
|
||||
# for each buy point
|
||||
for index, row in ticker[ticker.buy == 1].iterrows():
|
||||
trade = Trade(
|
||||
open_rate=row['close'],
|
||||
open_date=arrow.get(row['date']).datetime,
|
||||
amount=1,
|
||||
)
|
||||
# calculate win/lose forwards from buy point
|
||||
for index2, row2 in ticker[index:].iterrows():
|
||||
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
|
||||
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
|
||||
|
||||
trades.append((pair, current_profit, index2 - index))
|
||||
break
|
||||
|
||||
labels = ['currency', 'profit', 'duration']
|
||||
results = DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
print('====================== BACKTESTING REPORT ================================')
|
||||
|
||||
for pair in pairs:
|
||||
print('For currency {}:'.format(pair))
|
||||
print_results(results[results.currency == pair])
|
||||
print('TOTAL OVER ALL TRADES:')
|
||||
print_results(results)
|
126
freqtrade/tests/test_main.py
Normal file
126
freqtrade/tests/test_main.py
Normal file
@ -0,0 +1,126 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import copy
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.exchange import validate_pairs
|
||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
||||
get_target_bid
|
||||
from freqtrade.misc import CONF_SCHEMA
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
configuration = {
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
"dry_run": True,
|
||||
"minimal_roi": {
|
||||
"2880": 0.005,
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": True,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": [
|
||||
"BTC_ETH",
|
||||
"BTC_TKN",
|
||||
"BTC_TRST",
|
||||
"BTC_SWT",
|
||||
]
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": True,
|
||||
"token": "token",
|
||||
"chat_id": "chat_id"
|
||||
}
|
||||
}
|
||||
validate(configuration, CONF_SCHEMA)
|
||||
return configuration
|
||||
|
||||
def test_create_trade(conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
# Save state of current whitelist
|
||||
whitelist = copy.deepcopy(conf['bittrex']['pair_whitelist'])
|
||||
|
||||
init(conf, 'sqlite://')
|
||||
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
assert trade is not None
|
||||
assert trade.open_rate == 0.072661
|
||||
assert trade.pair == pair
|
||||
assert trade.exchange == exchange.Exchange.BITTREX
|
||||
assert trade.amount == 206.43811673387373
|
||||
assert trade.stake_amount == 15.0
|
||||
assert trade.is_open
|
||||
assert trade.open_date is not None
|
||||
assert whitelist == conf['bittrex']['pair_whitelist']
|
||||
|
||||
buy_signal.assert_has_calls(
|
||||
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
|
||||
)
|
||||
|
||||
def test_handle_trade(conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.17256061,
|
||||
'ask': 0.172661,
|
||||
'last': 0.17256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
assert trade
|
||||
handle_trade(trade)
|
||||
assert trade.close_rate == 0.17256061
|
||||
assert trade.close_profit == 137.4872490056564
|
||||
assert trade.close_date is not None
|
||||
assert trade.open_order_id == 'dry_run'
|
||||
|
||||
def test_close_trade(conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
assert trade
|
||||
|
||||
# Simulate that there is no open order
|
||||
trade.open_order_id = None
|
||||
|
||||
closed = close_trade_if_fulfilled(trade)
|
||||
assert closed
|
||||
assert not trade.is_open
|
||||
|
||||
def test_balance_fully_ask_side(mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
||||
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
||||
|
||||
def test_balance_fully_last_side(mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
||||
|
||||
def test_balance_when_last_bigger_than_ask(mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
20
freqtrade/tests/test_persistence.py
Normal file
20
freqtrade/tests/test_persistence.py
Normal file
@ -0,0 +1,20 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
def test_exec_sell_order(mocker):
|
||||
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
|
||||
trade = Trade(
|
||||
pair='BTC_ETH',
|
||||
stake_amount=1.00,
|
||||
open_rate=0.50,
|
||||
amount=10.00,
|
||||
exchange=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)
|
||||
assert profit == 100.0
|
||||
assert trade.close_rate == 1.0
|
||||
assert trade.close_profit == profit
|
||||
assert trade.close_date is not None
|
199
freqtrade/tests/test_telegram.py
Normal file
199
freqtrade/tests/test_telegram.py
Normal file
@ -0,0 +1,199 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
from telegram import Bot, Update, Message, Chat
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.main import init, create_trade
|
||||
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
configuration = {
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
"dry_run": True,
|
||||
"minimal_roi": {
|
||||
"2880": 0.005,
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": True,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": [
|
||||
"BTC_ETH"
|
||||
]
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": True,
|
||||
"token": "token",
|
||||
"chat_id": "0"
|
||||
},
|
||||
"initial_state": "running"
|
||||
}
|
||||
validate(configuration, CONF_SCHEMA)
|
||||
return configuration
|
||||
|
||||
@pytest.fixture
|
||||
def update():
|
||||
_update = Update(0)
|
||||
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
|
||||
return _update
|
||||
|
||||
|
||||
class MagicBot(MagicMock, Bot):
|
||||
pass
|
||||
|
||||
|
||||
def test_status_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_status(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_profit_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
assert trade
|
||||
trade.close_rate = 0.07256061
|
||||
trade.close_profit = 100.00
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.open_order_id = None
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_profit(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_forcesell_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
assert trade
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
update.message.text = '/forcesell 1'
|
||||
_forcesell(bot=MagicBot(), update=update)
|
||||
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||
assert '0.072561' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_performance_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
assert trade
|
||||
trade.close_rate = 0.07256061
|
||||
trade.close_profit = 100.00
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.open_order_id = None
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_performance(bot=MagicBot(), update=update)
|
||||
assert msg_mock.call_count == 2
|
||||
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
||||
assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0]
|
||||
|
||||
def test_start_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
update_state(State.STOPPED)
|
||||
assert get_state() == State.STOPPED
|
||||
_start(bot=MagicBot(), update=update)
|
||||
assert get_state() == State.RUNNING
|
||||
assert msg_mock.call_count == 0
|
||||
|
||||
def test_stop_handle(conf, update, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
||||
init(conf, 'sqlite://')
|
||||
|
||||
update_state(State.RUNNING)
|
||||
assert get_state() == State.RUNNING
|
||||
_stop(bot=MagicBot(), update=update)
|
||||
assert get_state() == State.STOPPED
|
||||
assert msg_mock.call_count == 1
|
||||
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
@ -1,14 +1,20 @@
|
||||
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
|
||||
-e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex
|
||||
SQLAlchemy==1.1.13
|
||||
python-telegram-bot==7.0.1
|
||||
python-telegram-bot==8.0
|
||||
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
|
||||
pytest==3.2.2
|
||||
pytest-mock==1.6.3
|
||||
pytest-cov==2.5.1
|
||||
|
||||
# Required for plotting data
|
||||
#matplotlib==2.0.2
|
||||
#PYQT5==5.9
|
5
setup.cfg
Normal file
5
setup.cfg
Normal file
@ -0,0 +1,5 @@
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
[tool:pytest]
|
||||
addopts = --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
|
41
setup.py
Normal file
41
setup.py
Normal file
@ -0,0 +1,41 @@
|
||||
from setuptools import setup
|
||||
|
||||
from freqtrade import __version__
|
||||
|
||||
|
||||
setup(name='freqtrade',
|
||||
version=__version__,
|
||||
description='Simple High Frequency Trading Bot for crypto currencies',
|
||||
url='https://github.com/gcarq/freqtrade',
|
||||
author='gcarq and contributors',
|
||||
author_email='michael.egger@tsn.at',
|
||||
license='GPLv3',
|
||||
packages=['freqtrade'],
|
||||
scripts=['bin/freqtrade'],
|
||||
setup_requires=['pytest-runner'],
|
||||
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
||||
install_requires=[
|
||||
'python-bittrex==0.1.3',
|
||||
'SQLAlchemy==1.1.13',
|
||||
'python-telegram-bot==8.0',
|
||||
'arrow==0.10.0',
|
||||
'requests==2.18.4',
|
||||
'urllib3==1.22',
|
||||
'wrapt==1.10.11',
|
||||
'pandas==0.20.3',
|
||||
'scikit-learn==0.19.0',
|
||||
'scipy==0.19.1',
|
||||
'jsonschema==2.6.0',
|
||||
'TA-Lib==0.4.10',
|
||||
],
|
||||
dependency_links=[
|
||||
"git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex-0.1.3"
|
||||
],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
'Topic :: Office/Business :: Financial :: Investment',
|
||||
'Intended Audience :: Science/Research',
|
||||
])
|
@ -1,49 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from pandas import DataFrame
|
||||
import arrow
|
||||
from analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, analyze_ticker, get_buy_signal
|
||||
|
||||
RESULT_BITTREX = {
|
||||
'success': True,
|
||||
'message': '',
|
||||
'result': [
|
||||
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 22.17210568, 'T': '2017-08-30T10:40:00', 'BV': 0.01448082},
|
||||
{'O': 0.00066194, 'H': 0.00066195, 'L': 0.00066194, 'C': 0.00066195, 'V': 33.4727437, 'T': '2017-08-30T10:34:00', 'BV': 0.02215696},
|
||||
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 53.85127609, 'T': '2017-08-30T10:37:00', 'BV': 0.0351708},
|
||||
{'O': 0.00066194, 'H': 0.00066194, 'L': 0.00065311, 'C': 0.00065311, 'V': 46.29210665, 'T': '2017-08-30T10:42:00', 'BV': 0.03063118},
|
||||
]
|
||||
}
|
||||
|
||||
class TestAnalyze(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.result = parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
|
||||
|
||||
def test_1_dataframe_has_correct_columns(self):
|
||||
self.assertEqual(self.result.columns.tolist(),
|
||||
['close', 'high', 'low', 'open', 'date', 'volume'])
|
||||
|
||||
def test_2_orders_by_date(self):
|
||||
self.assertEqual(self.result['date'].tolist(),
|
||||
['2017-08-30T10:34:00',
|
||||
'2017-08-30T10:37:00',
|
||||
'2017-08-30T10:40:00',
|
||||
'2017-08-30T10:42:00'])
|
||||
|
||||
def test_3_populates_buy_trend(self):
|
||||
dataframe = populate_buy_trend(populate_indicators(self.result))
|
||||
self.assertTrue('buy' in dataframe.columns)
|
||||
self.assertTrue('buy_price' in dataframe.columns)
|
||||
|
||||
def test_4_returns_latest_buy_signal(self):
|
||||
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||
with patch('analyze.analyze_ticker', return_value=buydf):
|
||||
self.assertEqual(get_buy_signal('BTC-ETH'), True)
|
||||
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||
with patch('analyze.analyze_ticker', return_value=buydf):
|
||||
self.assertEqual(get_buy_signal('BTC-ETH'), False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,71 +0,0 @@
|
||||
# pragma pylint: disable=missing-docstring
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
from analyze import analyze_ticker
|
||||
from persistence import Trade
|
||||
from main import should_sell
|
||||
|
||||
def print_results(results):
|
||||
print('Made {} buys. Average profit {:.1f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
||||
len(results.index),
|
||||
results.profit.mean() * 100.0,
|
||||
results.profit.sum(),
|
||||
results.duration.mean()*5
|
||||
))
|
||||
|
||||
class TestMain(unittest.TestCase):
|
||||
pairs = ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||
conf = {
|
||||
"minimal_roi": {
|
||||
"2880": 0.005,
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
},
|
||||
"stoploss": -0.10
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||
|
||||
@unittest.skipIf(not os.environ.get('BACKTEST', False), "slow, should be run manually")
|
||||
def test_backtest(self):
|
||||
trades = []
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
for pair in self.pairs:
|
||||
with open('test/testdata/'+pair+'.json') as data_file:
|
||||
data = json.load(data_file)
|
||||
|
||||
with patch('analyze.get_ticker', return_value=data):
|
||||
with patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')):
|
||||
ticker = analyze_ticker(pair)
|
||||
# for each buy point
|
||||
for index, row in ticker[ticker.buy == 1].iterrows():
|
||||
trade = Trade(
|
||||
open_rate=row['close'],
|
||||
open_date=arrow.get(row['date']).datetime,
|
||||
amount=1,
|
||||
)
|
||||
# calculate win/lose forwards from buy point
|
||||
for index2, row2 in ticker[index:].iterrows():
|
||||
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
|
||||
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
|
||||
|
||||
trades.append((pair, current_profit, index2 - index))
|
||||
break
|
||||
|
||||
labels = ['currency', 'profit', 'duration']
|
||||
results = DataFrame.from_records(trades, columns=labels)
|
||||
|
||||
print('====================== BACKTESTING REPORT ================================')
|
||||
|
||||
for pair in self.pairs:
|
||||
print('For currency {}:'.format(pair))
|
||||
print_results(results[results.currency == pair])
|
||||
print('TOTAL OVER ALL TRADES:')
|
||||
print_results(results)
|
@ -1,114 +0,0 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from jsonschema import validate
|
||||
|
||||
import exchange
|
||||
from main import create_trade, handle_trade, close_trade_if_fulfilled, init, get_target_bid
|
||||
from misc import CONF_SCHEMA
|
||||
from persistence import Trade
|
||||
|
||||
|
||||
class TestMain(unittest.TestCase):
|
||||
conf = {
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
"dry_run": True,
|
||||
"minimal_roi": {
|
||||
"2880": 0.005,
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": True,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": [
|
||||
"BTC_ETH"
|
||||
]
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": True,
|
||||
"token": "token",
|
||||
"chat_id": "chat_id"
|
||||
}
|
||||
}
|
||||
|
||||
def test_1_create_trade(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
with patch('main.get_buy_signal', side_effect=lambda _: True) as buy_signal:
|
||||
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
||||
with patch.multiple('main.exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id')):
|
||||
init(self.conf, 'sqlite://')
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
self.assertIsNotNone(trade)
|
||||
self.assertEqual(trade.open_rate, 0.072661)
|
||||
self.assertEqual(trade.pair, 'BTC_ETH')
|
||||
self.assertEqual(trade.exchange, exchange.Exchange.BITTREX)
|
||||
self.assertEqual(trade.amount, 206.43811673387373)
|
||||
self.assertEqual(trade.stake_amount, 15.0)
|
||||
self.assertEqual(trade.is_open, True)
|
||||
self.assertIsNotNone(trade.open_date)
|
||||
buy_signal.assert_called_once_with('BTC_ETH')
|
||||
|
||||
def test_2_handle_trade(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
|
||||
with patch.multiple('main.exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.17256061,
|
||||
'ask': 0.172661,
|
||||
'last': 0.17256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id')):
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
self.assertTrue(trade)
|
||||
handle_trade(trade)
|
||||
self.assertEqual(trade.close_rate, 0.17256061)
|
||||
self.assertEqual(trade.close_profit, 137.4872490056564)
|
||||
self.assertIsNotNone(trade.close_date)
|
||||
self.assertEqual(trade.open_order_id, 'dry_run')
|
||||
|
||||
def test_3_close_trade(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||
self.assertTrue(trade)
|
||||
|
||||
# Simulate that there is no open order
|
||||
trade.open_order_id = None
|
||||
|
||||
closed = close_trade_if_fulfilled(trade)
|
||||
self.assertTrue(closed)
|
||||
self.assertEqual(trade.is_open, False)
|
||||
|
||||
def test_balance_fully_ask_side(self):
|
||||
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}):
|
||||
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 20)
|
||||
|
||||
def test_balance_fully_last_side(self):
|
||||
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
|
||||
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 10)
|
||||
|
||||
def test_balance_when_last_bigger_than_ask(self):
|
||||
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
|
||||
self.assertEqual(get_target_bid({'ask': 5, 'last': 10}), 5)
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
validate(cls.conf, CONF_SCHEMA)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,28 +0,0 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from exchange import Exchange
|
||||
from persistence import Trade
|
||||
|
||||
|
||||
class TestTrade(unittest.TestCase):
|
||||
def test_1_exec_sell_order(self):
|
||||
with patch('main.exchange.sell', side_effect='mocked_order_id') as api_mock:
|
||||
trade = Trade(
|
||||
pair='BTC_ETH',
|
||||
stake_amount=1.00,
|
||||
open_rate=0.50,
|
||||
amount=10.00,
|
||||
exchange=Exchange.BITTREX,
|
||||
open_order_id='mocked'
|
||||
)
|
||||
profit = trade.exec_sell_order(1.00, 10.00)
|
||||
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
|
||||
self.assertEqual(profit, 100.0)
|
||||
self.assertEqual(trade.close_rate, 1.0)
|
||||
self.assertEqual(trade.close_profit, profit)
|
||||
self.assertIsNotNone(trade.close_date)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,195 +0,0 @@
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
from datetime import datetime
|
||||
|
||||
from jsonschema import validate
|
||||
from telegram import Bot, Update, Message, Chat
|
||||
|
||||
import exchange
|
||||
from main import init, create_trade
|
||||
from misc import CONF_SCHEMA, update_state, State, get_state
|
||||
from persistence import Trade
|
||||
from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
|
||||
|
||||
|
||||
class MagicBot(MagicMock, Bot):
|
||||
pass
|
||||
|
||||
|
||||
class TestTelegram(unittest.TestCase):
|
||||
|
||||
conf = {
|
||||
"max_open_trades": 3,
|
||||
"stake_currency": "BTC",
|
||||
"stake_amount": 0.05,
|
||||
"dry_run": True,
|
||||
"minimal_roi": {
|
||||
"2880": 0.005,
|
||||
"720": 0.01,
|
||||
"0": 0.02
|
||||
},
|
||||
"bid_strategy": {
|
||||
"ask_last_balance": 0.0
|
||||
},
|
||||
"bittrex": {
|
||||
"enabled": True,
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"pair_whitelist": [
|
||||
"BTC_ETH"
|
||||
]
|
||||
},
|
||||
"telegram": {
|
||||
"enabled": True,
|
||||
"token": "token",
|
||||
"chat_id": "0"
|
||||
},
|
||||
"initial_state": "running"
|
||||
}
|
||||
|
||||
def test_1_status_handle(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||
msg_mock = MagicMock()
|
||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||
with patch.multiple('main.exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id')):
|
||||
init(self.conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
self.assertTrue(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_status(bot=MagicBot(), update=self.update)
|
||||
self.assertEqual(msg_mock.call_count, 2)
|
||||
self.assertIn('[BTC_ETH]', msg_mock.call_args_list[-1][0][0])
|
||||
|
||||
def test_2_profit_handle(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||
msg_mock = MagicMock()
|
||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||
with patch.multiple('main.exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id')):
|
||||
init(self.conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
self.assertTrue(trade)
|
||||
trade.close_rate = 0.07256061
|
||||
trade.close_profit = 100.00
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.open_order_id = None
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_profit(bot=MagicBot(), update=self.update)
|
||||
self.assertEqual(msg_mock.call_count, 2)
|
||||
self.assertIn('(100.00%)', msg_mock.call_args_list[-1][0][0])
|
||||
|
||||
def test_3_forcesell_handle(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||
msg_mock = MagicMock()
|
||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||
with patch.multiple('main.exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id')):
|
||||
init(self.conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
self.assertTrue(trade)
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
self.update.message.text = '/forcesell 1'
|
||||
_forcesell(bot=MagicBot(), update=self.update)
|
||||
|
||||
self.assertEqual(msg_mock.call_count, 2)
|
||||
self.assertIn('Selling [BTC/ETH]', msg_mock.call_args_list[-1][0][0])
|
||||
self.assertIn('0.072561', msg_mock.call_args_list[-1][0][0])
|
||||
|
||||
def test_4_performance_handle(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
with patch('main.get_buy_signal', side_effect=lambda _: True):
|
||||
msg_mock = MagicMock()
|
||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||
with patch.multiple('main.exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id')):
|
||||
init(self.conf, 'sqlite://')
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
||||
self.assertTrue(trade)
|
||||
trade.close_rate = 0.07256061
|
||||
trade.close_profit = 100.00
|
||||
trade.close_date = datetime.utcnow()
|
||||
trade.open_order_id = None
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
_performance(bot=MagicBot(), update=self.update)
|
||||
self.assertEqual(msg_mock.call_count, 2)
|
||||
self.assertIn('Performance', msg_mock.call_args_list[-1][0][0])
|
||||
self.assertIn('BTC_ETH 100.00%', msg_mock.call_args_list[-1][0][0])
|
||||
|
||||
def test_5_start_handle(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
msg_mock = MagicMock()
|
||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||
init(self.conf, 'sqlite://')
|
||||
|
||||
update_state(State.STOPPED)
|
||||
self.assertEqual(get_state(), State.STOPPED)
|
||||
_start(bot=MagicBot(), update=self.update)
|
||||
self.assertEqual(get_state(), State.RUNNING)
|
||||
self.assertEqual(msg_mock.call_count, 0)
|
||||
|
||||
def test_6_stop_handle(self):
|
||||
with patch.dict('main._CONF', self.conf):
|
||||
msg_mock = MagicMock()
|
||||
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
|
||||
init(self.conf, 'sqlite://')
|
||||
|
||||
update_state(State.RUNNING)
|
||||
self.assertEqual(get_state(), State.RUNNING)
|
||||
_stop(bot=MagicBot(), update=self.update)
|
||||
self.assertEqual(get_state(), State.STOPPED)
|
||||
self.assertEqual(msg_mock.call_count, 1)
|
||||
self.assertIn('Stopping trader', msg_mock.call_args_list[0][0][0])
|
||||
|
||||
def setUp(self):
|
||||
self.update = Update(0)
|
||||
self.update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
validate(cls.conf, CONF_SCHEMA)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
Reference in New Issue
Block a user