diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 000000000..3d5e5889f --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = freqtrade/tests/* diff --git a/.travis.yml b/.travis.yml index a31c421f9..461324f45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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= diff --git a/Dockerfile b/Dockerfile index f03d75e4d..bd303769d 100644 --- a/Dockerfile +++ b/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"] diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..b018ee274 --- /dev/null +++ b/MANIFEST.in @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 954ddaf8e..3a2101d64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # freqtrade [![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade) +[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop)](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 diff --git a/bin/freqtrade b/bin/freqtrade new file mode 100755 index 000000000..3ecd3a520 --- /dev/null +++ b/bin/freqtrade @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +from freqtrade.main import main +main() \ No newline at end of file diff --git a/config.json.example b/config.json.example index a0b93c455..c2f6668d4 100644 --- a/config.json.example +++ b/config.json.example @@ -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 }, diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py new file mode 100644 index 000000000..98f461790 --- /dev/null +++ b/freqtrade/__init__.py @@ -0,0 +1,3 @@ +__version__ = '0.10.0' + +from . import main diff --git a/analyze.py b/freqtrade/analyze.py similarity index 77% rename from analyze.py rename to freqtrade/analyze.py index 338549c87..d8b86d945 100644 --- a/analyze.py +++ b/freqtrade/analyze.py @@ -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) diff --git a/exchange.py b/freqtrade/exchange.py similarity index 93% rename from exchange.py rename to freqtrade/exchange.py index 2b0947311..96bc13159 100644 --- a/exchange.py +++ b/freqtrade/exchange.py @@ -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: diff --git a/main.py b/freqtrade/main.py similarity index 92% rename from main.py rename to freqtrade/main.py index 8f2256bb4..518c82a6d 100755 --- a/main.py +++ b/freqtrade/main.py @@ -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() diff --git a/misc.py b/freqtrade/misc.py similarity index 100% rename from misc.py rename to freqtrade/misc.py diff --git a/persistence.py b/freqtrade/persistence.py similarity index 98% rename from persistence.py rename to freqtrade/persistence.py index dd917a750..4f7129a9c 100644 --- a/persistence.py +++ b/freqtrade/persistence.py @@ -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 = {} diff --git a/rpc/__init__.py b/freqtrade/rpc/__init__.py similarity index 100% rename from rpc/__init__.py rename to freqtrade/rpc/__init__.py diff --git a/rpc/telegram.py b/freqtrade/rpc/telegram.py similarity index 98% rename from rpc/telegram.py rename to freqtrade/rpc/telegram.py index cf764a65c..80e330a51 100644 --- a/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -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 = [ diff --git a/test/__init__.py b/freqtrade/tests/__init__.py similarity index 100% rename from test/__init__.py rename to freqtrade/tests/__init__.py diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py new file mode 100644 index 000000000..e716eb3ad --- /dev/null +++ b/freqtrade/tests/test_analyze.py @@ -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') diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py new file mode 100644 index 000000000..293b752b4 --- /dev/null +++ b/freqtrade/tests/test_backtesting.py @@ -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) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py new file mode 100644 index 000000000..63da42e37 --- /dev/null +++ b/freqtrade/tests/test_main.py @@ -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 diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py new file mode 100644 index 000000000..376825c72 --- /dev/null +++ b/freqtrade/tests/test_persistence.py @@ -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 diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py new file mode 100644 index 000000000..e01c5060a --- /dev/null +++ b/freqtrade/tests/test_telegram.py @@ -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] diff --git a/test/testdata/btc-edg.json b/freqtrade/tests/testdata/btc-edg.json similarity index 100% rename from test/testdata/btc-edg.json rename to freqtrade/tests/testdata/btc-edg.json diff --git a/test/testdata/btc-etc.json b/freqtrade/tests/testdata/btc-etc.json similarity index 100% rename from test/testdata/btc-etc.json rename to freqtrade/tests/testdata/btc-etc.json diff --git a/test/testdata/btc-eth.json b/freqtrade/tests/testdata/btc-eth.json similarity index 100% rename from test/testdata/btc-eth.json rename to freqtrade/tests/testdata/btc-eth.json diff --git a/test/testdata/btc-ltc.json b/freqtrade/tests/testdata/btc-ltc.json similarity index 100% rename from test/testdata/btc-ltc.json rename to freqtrade/tests/testdata/btc-ltc.json diff --git a/test/testdata/btc-mtl.json b/freqtrade/tests/testdata/btc-mtl.json similarity index 100% rename from test/testdata/btc-mtl.json rename to freqtrade/tests/testdata/btc-mtl.json diff --git a/test/testdata/btc-neo.json b/freqtrade/tests/testdata/btc-neo.json similarity index 100% rename from test/testdata/btc-neo.json rename to freqtrade/tests/testdata/btc-neo.json diff --git a/test/testdata/btc-omg.json b/freqtrade/tests/testdata/btc-omg.json similarity index 100% rename from test/testdata/btc-omg.json rename to freqtrade/tests/testdata/btc-omg.json diff --git a/test/testdata/btc-pay.json b/freqtrade/tests/testdata/btc-pay.json similarity index 100% rename from test/testdata/btc-pay.json rename to freqtrade/tests/testdata/btc-pay.json diff --git a/test/testdata/btc-pivx.json b/freqtrade/tests/testdata/btc-pivx.json similarity index 100% rename from test/testdata/btc-pivx.json rename to freqtrade/tests/testdata/btc-pivx.json diff --git a/test/testdata/btc-qtum.json b/freqtrade/tests/testdata/btc-qtum.json similarity index 100% rename from test/testdata/btc-qtum.json rename to freqtrade/tests/testdata/btc-qtum.json diff --git a/requirements.txt b/requirements.txt index df6e07226..b7bf943ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..ea59030de --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[aliases] +test=pytest + +[tool:pytest] +addopts = --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..e89fd2eee --- /dev/null +++ b/setup.py @@ -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', + ]) diff --git a/test/test_analyze.py b/test/test_analyze.py deleted file mode 100644 index 9fdc16d7a..000000000 --- a/test/test_analyze.py +++ /dev/null @@ -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() diff --git a/test/test_backtesting.py b/test/test_backtesting.py deleted file mode 100644 index fdf18cc32..000000000 --- a/test/test_backtesting.py +++ /dev/null @@ -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) diff --git a/test/test_main.py b/test/test_main.py deleted file mode 100644 index 9ff4f97f2..000000000 --- a/test/test_main.py +++ /dev/null @@ -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() diff --git a/test/test_persistence.py b/test/test_persistence.py deleted file mode 100644 index 8875b0e78..000000000 --- a/test/test_persistence.py +++ /dev/null @@ -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() diff --git a/test/test_telegram.py b/test/test_telegram.py deleted file mode 100644 index e7af1a876..000000000 --- a/test/test_telegram.py +++ /dev/null @@ -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()