From 0e5edd08e5f34898f8d17558bdcf1992a4d1256d Mon Sep 17 00:00:00 2001 From: Eoin Date: Wed, 27 Sep 2017 23:43:32 +0100 Subject: [PATCH 01/57] add dataframe empty check --- analyze.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/analyze.py b/analyze.py index 338549c87..51e84c6cd 100644 --- a/analyze.py +++ b/analyze.py @@ -96,6 +96,11 @@ def analyze_ticker(pair: str) -> DataFrame: minimum_date = arrow.utcnow().shift(hours=-6) data = get_ticker(pair, minimum_date) dataframe = parse_ticker_dataframe(data['result'], minimum_date) + + if dataframe.empty: + logger.debug('Empty dataframe for pair %s', pair) + return dataframe + dataframe = populate_indicators(dataframe) dataframe = populate_buy_trend(dataframe) return dataframe @@ -107,6 +112,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 From a45073997de3285af4da103982e50cf60e1203ee Mon Sep 17 00:00:00 2001 From: Eoin Date: Thu, 28 Sep 2017 20:07:33 +0100 Subject: [PATCH 02/57] review comments: change log to warning --- analyze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyze.py b/analyze.py index 51e84c6cd..f344f047a 100644 --- a/analyze.py +++ b/analyze.py @@ -98,7 +98,7 @@ def analyze_ticker(pair: str) -> DataFrame: dataframe = parse_ticker_dataframe(data['result'], minimum_date) if dataframe.empty: - logger.debug('Empty dataframe for pair %s', pair) + logger.warning('Empty dataframe for pair %s', pair) return dataframe dataframe = populate_indicators(dataframe) From 2df2041d53245586b69ec88884bf3449e8f82bdd Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Fri, 29 Sep 2017 00:15:38 +0200 Subject: [PATCH 03/57] Telegram: Fix being optional --- rpc/telegram.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rpc/telegram.py b/rpc/telegram.py index cf764a65c..7bb6881c1 100644 --- a/rpc/telegram.py +++ b/rpc/telegram.py @@ -31,9 +31,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 = [ From 272abed8073053d2d81e336a0bd5c6d2736e63b8 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 29 Sep 2017 09:34:20 +0300 Subject: [PATCH 04/57] show two decimals in average profit in backtesting results --- test/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_backtesting.py b/test/test_backtesting.py index fdf18cc32..ad25e17a8 100644 --- a/test/test_backtesting.py +++ b/test/test_backtesting.py @@ -11,7 +11,7 @@ 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( + 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(), From b2f4778352a2419be7c86a9e288593892d68a4d2 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 29 Sep 2017 09:37:05 +0300 Subject: [PATCH 05/57] show last 24hours in analyze graph --- analyze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyze.py b/analyze.py index f344f047a..9c0f4bc11 100644 --- a/analyze.py +++ b/analyze.py @@ -93,7 +93,7 @@ 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) From b97f0f07058109504ef0dd1384be68708d6ecd20 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 29 Sep 2017 09:37:29 +0300 Subject: [PATCH 06/57] use btc-eth as default pair for analyze graph --- analyze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analyze.py b/analyze.py index 9c0f4bc11..774a900f9 100644 --- a/analyze.py +++ b/analyze.py @@ -165,7 +165,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) From 44cdf3e0c2ecda7ab99f22f476c8121533251984 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 29 Sep 2017 09:37:45 +0300 Subject: [PATCH 07/57] improved buy signal strategy --- analyze.py | 44 +++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/analyze.py b/analyze.py index f344f047a..16c9efd5c 100644 --- a/analyze.py +++ b/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'] @@ -147,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 From c9226a329ca51c046d61361ab7c285a411aaa3d8 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 29 Sep 2017 09:33:40 +0300 Subject: [PATCH 08/57] adjust roi and stop loss for backtesting --- test/test_backtesting.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/test_backtesting.py b/test/test_backtesting.py index ad25e17a8..1bd7fed54 100644 --- a/test/test_backtesting.py +++ b/test/test_backtesting.py @@ -22,11 +22,12 @@ 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 + "60": 0.0, + "40": 0.01, + "20": 0.02, + "0": 0.03 }, - "stoploss": -0.10 + "stoploss": -0.40 } @classmethod From 1f1e64560aca5d7959fc2b29833bc3aa90f9a918 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 29 Sep 2017 09:58:00 +0300 Subject: [PATCH 09/57] adjust roi and stop loss in config.json.example --- config.json.example | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/config.json.example b/config.json.example index a0b93c455..584fb019d 100644 --- a/config.json.example +++ b/config.json.example @@ -3,13 +3,14 @@ "stake_currency": "BTC", "stake_amount": 0.05, "dry_run": false, - "minimal_roi": { - "2880": 0.005, - "720": 0.01, - "0": 0.02 - }, - "stoploss": -0.10, - "bid_strategy": { + "minimal_roi": { + "60": 0.0, + "40": 0.01, + "20": 0.02, + "0": 0.03 + }, + "stoploss": -0.40, + "bid_strategy": { "ask_last_balance": 0.0 }, "bittrex": { From d045116297289d23395f1e67a7f6d1a962531840 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 29 Sep 2017 18:59:05 +0300 Subject: [PATCH 10/57] upgraded to latest telegram library (8.0) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index df6e07226..03004b7de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -e git+https://github.com/ericsomdahl/python-bittrex.git#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 From b225b0cb90ee36be19321ef9d6dcdc5a8314d109 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 29 Sep 2017 19:07:25 +0200 Subject: [PATCH 11/57] remove python nightly interpreter --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a31c421f9..b76fe98ab 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: From 0c517ee3b6ebfb8dac0d3a801634ed9f6f2c9464 Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 28 Sep 2017 23:26:28 +0200 Subject: [PATCH 12/57] move project into freqtrade/ --- freqtrade/__init__.py | 1 + analyze.py => freqtrade/analyze.py | 0 exchange.py => freqtrade/exchange.py | 0 main.py => freqtrade/main.py | 16 ++---- misc.py => freqtrade/misc.py | 0 persistence.py => freqtrade/persistence.py | 4 +- {rpc => freqtrade/rpc}/__init__.py | 0 {rpc => freqtrade/rpc}/telegram.py | 9 ++-- {test => freqtrade/tests}/__init__.py | 0 {test => freqtrade/tests}/test_analyze.py | 11 ++-- {test => freqtrade/tests}/test_backtesting.py | 21 ++++---- {test => freqtrade/tests}/test_main.py | 31 +++++------ {test => freqtrade/tests}/test_persistence.py | 6 +-- {test => freqtrade/tests}/test_telegram.py | 52 +++++++++---------- .../tests}/testdata/btc-edg.json | 0 .../tests}/testdata/btc-etc.json | 0 .../tests}/testdata/btc-eth.json | 0 .../tests}/testdata/btc-ltc.json | 0 .../tests}/testdata/btc-mtl.json | 0 .../tests}/testdata/btc-neo.json | 0 .../tests}/testdata/btc-omg.json | 0 .../tests}/testdata/btc-pay.json | 0 .../tests}/testdata/btc-pivx.json | 0 .../tests}/testdata/btc-qtum.json | 0 24 files changed, 75 insertions(+), 76 deletions(-) create mode 100644 freqtrade/__init__.py rename analyze.py => freqtrade/analyze.py (100%) rename exchange.py => freqtrade/exchange.py (100%) rename main.py => freqtrade/main.py (97%) rename misc.py => freqtrade/misc.py (100%) rename persistence.py => freqtrade/persistence.py (98%) rename {rpc => freqtrade/rpc}/__init__.py (100%) rename {rpc => freqtrade/rpc}/telegram.py (98%) rename {test => freqtrade/tests}/__init__.py (100%) rename {test => freqtrade/tests}/test_analyze.py (88%) rename {test => freqtrade/tests}/test_backtesting.py (88%) rename {test => freqtrade/tests}/test_main.py (77%) rename {test => freqtrade/tests}/test_persistence.py (81%) rename {test => freqtrade/tests}/test_telegram.py (78%) rename {test => freqtrade/tests}/testdata/btc-edg.json (100%) rename {test => freqtrade/tests}/testdata/btc-etc.json (100%) rename {test => freqtrade/tests}/testdata/btc-eth.json (100%) rename {test => freqtrade/tests}/testdata/btc-ltc.json (100%) rename {test => freqtrade/tests}/testdata/btc-mtl.json (100%) rename {test => freqtrade/tests}/testdata/btc-neo.json (100%) rename {test => freqtrade/tests}/testdata/btc-omg.json (100%) rename {test => freqtrade/tests}/testdata/btc-pay.json (100%) rename {test => freqtrade/tests}/testdata/btc-pivx.json (100%) rename {test => freqtrade/tests}/testdata/btc-qtum.json (100%) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py new file mode 100644 index 000000000..9d1bb721b --- /dev/null +++ b/freqtrade/__init__.py @@ -0,0 +1 @@ +__version__ = '0.10.0' diff --git a/analyze.py b/freqtrade/analyze.py similarity index 100% rename from analyze.py rename to freqtrade/analyze.py diff --git a/exchange.py b/freqtrade/exchange.py similarity index 100% rename from exchange.py rename to freqtrade/exchange.py diff --git a/main.py b/freqtrade/main.py similarity index 97% rename from main.py rename to freqtrade/main.py index 8f2256bb4..4c0fb1401 100755 --- a/main.py +++ b/freqtrade/main.py @@ -8,22 +8,16 @@ from typing import Dict, Optional 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 = {} 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 7bb6881c1..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) 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/test/test_analyze.py b/freqtrade/tests/test_analyze.py similarity index 88% rename from test/test_analyze.py rename to freqtrade/tests/test_analyze.py index 9fdc16d7a..d463ba560 100644 --- a/test/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -1,9 +1,12 @@ # 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 +from pandas import DataFrame + +from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \ + get_buy_signal RESULT_BITTREX = { 'success': True, @@ -38,10 +41,10 @@ class TestAnalyze(unittest.TestCase): def test_4_returns_latest_buy_signal(self): buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) - with patch('analyze.analyze_ticker', return_value=buydf): + with patch('freqtrade.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): + with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): self.assertEqual(get_buy_signal('BTC-ETH'), False) diff --git a/test/test_backtesting.py b/freqtrade/tests/test_backtesting.py similarity index 88% rename from test/test_backtesting.py rename to freqtrade/tests/test_backtesting.py index 1bd7fed54..ff8f8a039 100644 --- a/test/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -1,14 +1,17 @@ # pragma pylint: disable=missing-docstring -import unittest -from unittest.mock import patch -import os import json import logging +import os +import unittest +from unittest.mock import patch + import arrow from pandas import DataFrame -from analyze import analyze_ticker -from persistence import Trade -from main import should_sell + +from freqtrade.analyze import analyze_ticker +from freqtrade.main import should_sell +from freqtrade.persistence import Trade + def print_results(results): print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format( @@ -37,12 +40,12 @@ class TestMain(unittest.TestCase): @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): + with patch.dict('freqtrade.main._CONF', self.conf): for pair in self.pairs: - with open('test/testdata/'+pair+'.json') as data_file: + with open('testdata/'+pair+'.json') as data_file: data = json.load(data_file) - with patch('analyze.get_ticker', return_value=data): + with patch('freqtrade.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 diff --git a/test/test_main.py b/freqtrade/tests/test_main.py similarity index 77% rename from test/test_main.py rename to freqtrade/tests/test_main.py index 9ff4f97f2..deadc7d62 100644 --- a/test/test_main.py +++ b/freqtrade/tests/test_main.py @@ -3,10 +3,11 @@ 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 +from freqtrade import exchange +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 class TestMain(unittest.TestCase): @@ -39,10 +40,10 @@ class TestMain(unittest.TestCase): } 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', + with patch.dict('freqtrade.main._CONF', self.conf): + with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) as buy_signal: + with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): + with patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, @@ -64,9 +65,9 @@ class TestMain(unittest.TestCase): 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', + with patch.dict('freqtrade.main._CONF', self.conf): + with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): + with patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock(return_value={ 'bid': 0.17256061, 'ask': 0.172661, @@ -82,7 +83,7 @@ class TestMain(unittest.TestCase): self.assertEqual(trade.open_order_id, 'dry_run') def test_3_close_trade(self): - with patch.dict('main._CONF', self.conf): + with patch.dict('freqtrade.main._CONF', self.conf): trade = Trade.query.filter(Trade.is_open.is_(True)).first() self.assertTrue(trade) @@ -94,15 +95,15 @@ class TestMain(unittest.TestCase): 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}}): + with patch.dict('freqtrade.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}}): + with patch.dict('freqtrade.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}}): + with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): self.assertEqual(get_target_bid({'ask': 5, 'last': 10}), 5) @classmethod diff --git a/test/test_persistence.py b/freqtrade/tests/test_persistence.py similarity index 81% rename from test/test_persistence.py rename to freqtrade/tests/test_persistence.py index 8875b0e78..4fd320b93 100644 --- a/test/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,13 +1,13 @@ import unittest from unittest.mock import patch -from exchange import Exchange -from persistence import Trade +from freqtrade.exchange import Exchange +from freqtrade.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: + with patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id') as api_mock: trade = Trade( pair='BTC_ETH', stake_amount=1.00, diff --git a/test/test_telegram.py b/freqtrade/tests/test_telegram.py similarity index 78% rename from test/test_telegram.py rename to freqtrade/tests/test_telegram.py index e7af1a876..af96a3045 100644 --- a/test/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -1,15 +1,15 @@ import unittest -from unittest.mock import patch, MagicMock from datetime import datetime +from unittest.mock import patch, MagicMock 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 +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 class MagicBot(MagicMock, Bot): @@ -48,11 +48,11 @@ class TestTelegram(unittest.TestCase): } def test_1_status_handle(self): - with patch.dict('main._CONF', self.conf): - with patch('main.get_buy_signal', side_effect=lambda _: True): + with patch.dict('freqtrade.main._CONF', self.conf): + with patch('freqtrade.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', + with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, @@ -72,11 +72,11 @@ class TestTelegram(unittest.TestCase): 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): + with patch.dict('freqtrade.main._CONF', self.conf): + with patch('freqtrade.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', + with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, @@ -101,11 +101,11 @@ class TestTelegram(unittest.TestCase): 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): + with patch.dict('freqtrade.main._CONF', self.conf): + with patch('freqtrade.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', + with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, @@ -128,11 +128,11 @@ class TestTelegram(unittest.TestCase): 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): + with patch.dict('freqtrade.main._CONF', self.conf): + with patch('freqtrade.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', + with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, @@ -158,9 +158,9 @@ class TestTelegram(unittest.TestCase): 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): + with patch.dict('freqtrade.main._CONF', self.conf): msg_mock = MagicMock() - with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): init(self.conf, 'sqlite://') update_state(State.STOPPED) @@ -170,9 +170,9 @@ class TestTelegram(unittest.TestCase): self.assertEqual(msg_mock.call_count, 0) def test_6_stop_handle(self): - with patch.dict('main._CONF', self.conf): + with patch.dict('freqtrade.main._CONF', self.conf): msg_mock = MagicMock() - with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): init(self.conf, 'sqlite://') update_state(State.RUNNING) 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 From 00499fa0d7bccca0469d5910b9ccc1156fa501cc Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 28 Sep 2017 23:26:56 +0200 Subject: [PATCH 13/57] add setup.py --- setup.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..79aacb7f9 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +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'], + zip_safe=False) From 998a887736e987a3203a007a30fa843fb3f93381 Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 28 Sep 2017 23:47:51 +0200 Subject: [PATCH 14/57] add command line script --- bin/freqtrade | 4 ++++ freqtrade/__init__.py | 2 ++ freqtrade/main.py | 13 +++++++++++-- setup.py | 1 + 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 bin/freqtrade diff --git a/bin/freqtrade b/bin/freqtrade new file mode 100644 index 000000000..fc085a11d --- /dev/null +++ b/bin/freqtrade @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +import freqtrade +freqtrade.main() \ No newline at end of file diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 9d1bb721b..c905819a2 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1 +1,3 @@ __version__ = '0.10.0' + +from .main import main diff --git a/freqtrade/main.py b/freqtrade/main.py index 4c0fb1401..c2a26ea38 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -232,7 +232,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 """ @@ -263,8 +263,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/setup.py b/setup.py index 79aacb7f9..0d8675d96 100644 --- a/setup.py +++ b/setup.py @@ -11,4 +11,5 @@ setup(name='freqtrade', author_email='michael.egger@tsn.at', license='GPLv3', packages=['freqtrade'], + scripts=['bin/freqtrade'], zip_safe=False) From 09b27d2094820726fd13b5d8b58079791bb7a53a Mon Sep 17 00:00:00 2001 From: gcarq Date: Thu, 28 Sep 2017 23:53:19 +0200 Subject: [PATCH 15/57] add manifest file --- MANIFEST.in | 3 +++ setup.py | 1 + 2 files changed, 4 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..c41afdcf9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include README.md +include config.json.example \ No newline at end of file diff --git a/setup.py b/setup.py index 0d8675d96..3cd1b3570 100644 --- a/setup.py +++ b/setup.py @@ -12,4 +12,5 @@ setup(name='freqtrade', license='GPLv3', packages=['freqtrade'], scripts=['bin/freqtrade'], + include_package_data=True, zip_safe=False) From 9c6c21637db02f6056f579dbf3ec06577b014292 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 29 Sep 2017 00:18:56 +0200 Subject: [PATCH 16/57] fix testsuite --- bin/freqtrade | 4 ++-- freqtrade/__init__.py | 2 +- setup.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/freqtrade b/bin/freqtrade index fc085a11d..3ecd3a520 100644 --- a/bin/freqtrade +++ b/bin/freqtrade @@ -1,4 +1,4 @@ #!/usr/bin/env python -import freqtrade -freqtrade.main() \ No newline at end of file +from freqtrade.main import main +main() \ No newline at end of file diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index c905819a2..98f461790 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,3 +1,3 @@ __version__ = '0.10.0' -from .main import main +from . import main diff --git a/setup.py b/setup.py index 3cd1b3570..d04a8a233 100644 --- a/setup.py +++ b/setup.py @@ -12,5 +12,7 @@ setup(name='freqtrade', license='GPLv3', packages=['freqtrade'], scripts=['bin/freqtrade'], + test_suite='nose.collector', + tests_require=['nose'], include_package_data=True, zip_safe=False) From 04bba626a8d27228fc2719269e9d425b80afd94e Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 29 Sep 2017 20:07:50 +0200 Subject: [PATCH 17/57] define install_requires for package distribution --- requirements.txt | 6 ++++-- setup.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 03004b7de..c6a1e399b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ --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==8.0 arrow==0.10.0 @@ -6,9 +6,11 @@ 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 TA-Lib==0.4.10 + +# Required for plotting data +#matplotlib==2.0.2 #PYQT5==5.9 \ No newline at end of file diff --git a/setup.py b/setup.py index d04a8a233..0442cf97c 100644 --- a/setup.py +++ b/setup.py @@ -14,5 +14,22 @@ setup(name='freqtrade', scripts=['bin/freqtrade'], test_suite='nose.collector', tests_require=['nose'], + 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) From df4da75535562369341599d351108095991f1317 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 29 Sep 2017 20:15:54 +0200 Subject: [PATCH 18/57] add pypi classifiers --- setup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0442cf97c..46d7a15a2 100644 --- a/setup.py +++ b/setup.py @@ -32,4 +32,10 @@ setup(name='freqtrade', "git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex-0.1.3" ], include_package_data=True, - zip_safe=False) + 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', + ]) From 8d3a6279b209a1b0c2257ea0748c1f96fe150cc7 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 15:58:31 +0200 Subject: [PATCH 19/57] use pytest --- freqtrade/main.py | 3 ++- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index c2a26ea38..6c80d490c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -91,7 +91,8 @@ def execute_sell(trade: Trade, current_rate: float) -> None: whitelist = _CONF[trade.exchange.name.lower()]['pair_whitelist'] profit = trade.exec_sell_order(current_rate, balance) - whitelist.append(trade.pair) + if trade.pair not in whitelist: + whitelist.append(trade.pair) message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( trade.exchange.name, trade.pair.replace('_', '/'), diff --git a/setup.py b/setup.py index 46d7a15a2..2d8d40990 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,8 @@ setup(name='freqtrade', license='GPLv3', packages=['freqtrade'], scripts=['bin/freqtrade'], - test_suite='nose.collector', - tests_require=['nose'], + setup_requires=['pytest-runner'], + tests_require=['pytest'], install_requires=[ 'python-bittrex==0.1.3', 'SQLAlchemy==1.1.13', From bbef0edcd1d6b2375e7af8693566924e02e78bdf Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 17:06:15 +0200 Subject: [PATCH 20/57] add coveralls.io to measure code quality --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b76fe98ab..aa1e01d31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,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 +- coverage run --source=freqtrade setup.py test +after_success: +- coveralls notifications: slack: secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= From b85b91365776ce78988c4b1e71e556d4849de804 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 17:13:08 +0200 Subject: [PATCH 21/57] revert coveralls --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa1e01d31..b76fe98ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,12 +15,9 @@ 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: -- coverage run --source=freqtrade setup.py test -after_success: -- coveralls +- python -m unittest notifications: slack: secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= From 898ab5a3707148675a9dd83403efe299dba7636a Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 17:01:12 +0200 Subject: [PATCH 22/57] implement test to reproduce it --- freqtrade/tests/test_main.py | 40 +++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index deadc7d62..fc8d40c84 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,6 +1,7 @@ import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import patch, MagicMock, call +import copy from jsonschema import validate from freqtrade import exchange @@ -29,7 +30,10 @@ class TestMain(unittest.TestCase): "key": "key", "secret": "secret", "pair_whitelist": [ - "BTC_ETH" + "BTC_ETH", + "BTC_TKN", + "BTC_TRST", + "BTC_SWT", ] }, "telegram": { @@ -50,19 +54,27 @@ class TestMain(unittest.TestCase): 'last': 0.07256061 }), buy=MagicMock(return_value='mocked_order_id')): + # Save state of current whitelist + whitelist = copy.deepcopy(self.conf['bittrex']['pair_whitelist']) + 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') + 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() + self.assertIsNotNone(trade) + self.assertEqual(trade.open_rate, 0.072661) + self.assertEqual(trade.pair, pair) + 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) + self.assertEqual(whitelist, self.conf['bittrex']['pair_whitelist']) + + buy_signal.assert_has_calls( + [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] + ) def test_2_handle_trade(self): with patch.dict('freqtrade.main._CONF', self.conf): From 4b42e1af4b9738269e895d344aede71508133945 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 17:02:07 +0200 Subject: [PATCH 23/57] use deepcopy --- freqtrade/main.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 6c80d490c..518c82a6d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -6,6 +6,7 @@ import traceback from datetime import datetime from typing import Dict, Optional +import copy from jsonschema import validate from freqtrade import exchange, persistence, __version__ @@ -88,11 +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) - if trade.pair not in whitelist: - whitelist.append(trade.pair) message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( trade.exchange.name, trade.pair.replace('_', '/'), @@ -161,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( @@ -169,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) From f409bdbba867696331442c65d41e40b2e5c4e5fc Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 18:55:48 +0200 Subject: [PATCH 24/57] add coveralls.io to measure code quality --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b76fe98ab..aa1e01d31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,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 +- coverage run --source=freqtrade setup.py test +after_success: +- coveralls notifications: slack: secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q= From 3f6f502e6631683a18a148cd50942c888a530a28 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 19:05:37 +0200 Subject: [PATCH 25/57] add code coverage badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 954ddaf8e..3dc649d06 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. From 53b4c3722e46c5b6d00584fb0dd04c657cce9be3 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 30 Sep 2017 20:38:19 +0300 Subject: [PATCH 26/57] convert asserts to pytest style --- freqtrade/tests/test_analyze.py | 16 +++++------ freqtrade/tests/test_main.py | 40 +++++++++++++-------------- freqtrade/tests/test_persistence.py | 8 +++--- freqtrade/tests/test_telegram.py | 42 ++++++++++++++--------------- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index d463ba560..74e945da2 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -24,28 +24,28 @@ class TestAnalyze(unittest.TestCase): 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']) + assert self.result.columns.tolist() == \ + ['close', 'high', 'low', 'open', 'date', 'volume'] def test_2_orders_by_date(self): - self.assertEqual(self.result['date'].tolist(), + assert self.result['date'].tolist() == \ ['2017-08-30T10:34:00', '2017-08-30T10:37:00', '2017-08-30T10:40:00', - '2017-08-30T10:42: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) + assert 'buy' in dataframe.columns + assert 'buy_price' in dataframe.columns def test_4_returns_latest_buy_signal(self): buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): - self.assertEqual(get_buy_signal('BTC-ETH'), True) + assert get_buy_signal('BTC-ETH') == True buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): - self.assertEqual(get_buy_signal('BTC-ETH'), False) + assert get_buy_signal('BTC-ETH') == False if __name__ == '__main__': diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index fc8d40c84..73bb153de 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -62,15 +62,15 @@ class TestMain(unittest.TestCase): 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, pair) - 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) - self.assertEqual(whitelist, self.conf['bittrex']['pair_whitelist']) + 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 == True + assert trade.open_date is not None + assert whitelist == self.conf['bittrex']['pair_whitelist'] buy_signal.assert_has_calls( [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] @@ -87,36 +87,36 @@ class TestMain(unittest.TestCase): }), buy=MagicMock(return_value='mocked_order_id')): trade = Trade.query.filter(Trade.is_open.is_(True)).first() - self.assertTrue(trade) + assert 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') + 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_3_close_trade(self): with patch.dict('freqtrade.main._CONF', self.conf): trade = Trade.query.filter(Trade.is_open.is_(True)).first() - self.assertTrue(trade) + assert 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) + assert closed + assert trade.is_open == False def test_balance_fully_ask_side(self): with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}): - self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 20) + assert get_target_bid({'ask': 20, 'last': 10}) == 20 def test_balance_fully_last_side(self): with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): - self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 10) + assert get_target_bid({'ask': 20, 'last': 10}) == 10 def test_balance_when_last_bigger_than_ask(self): with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): - self.assertEqual(get_target_bid({'ask': 5, 'last': 10}), 5) + assert get_target_bid({'ask': 5, 'last': 10}) == 5 @classmethod def setUpClass(cls): diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 4fd320b93..5b7c94989 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -18,10 +18,10 @@ class TestTrade(unittest.TestCase): ) 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) + assert profit == 100.0 + assert trade.close_rate == 1.0 + assert trade.close_profit == profit + assert trade.close_date is not None if __name__ == '__main__': diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index af96a3045..f86090d35 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -63,13 +63,13 @@ class TestTelegram(unittest.TestCase): # Create some test data trade = create_trade(15.0, exchange.Exchange.BITTREX) - self.assertTrue(trade) + assert 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]) + assert msg_mock.call_count == 2 + assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] def test_2_profit_handle(self): with patch.dict('freqtrade.main._CONF', self.conf): @@ -87,7 +87,7 @@ class TestTelegram(unittest.TestCase): # Create some test data trade = create_trade(15.0, exchange.Exchange.BITTREX) - self.assertTrue(trade) + assert trade trade.close_rate = 0.07256061 trade.close_profit = 100.00 trade.close_date = datetime.utcnow() @@ -97,8 +97,8 @@ class TestTelegram(unittest.TestCase): 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]) + assert msg_mock.call_count == 2 + assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] def test_3_forcesell_handle(self): with patch.dict('freqtrade.main._CONF', self.conf): @@ -116,16 +116,16 @@ class TestTelegram(unittest.TestCase): # Create some test data trade = create_trade(15.0, exchange.Exchange.BITTREX) - self.assertTrue(trade) + assert 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]) + 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_4_performance_handle(self): with patch.dict('freqtrade.main._CONF', self.conf): @@ -143,7 +143,7 @@ class TestTelegram(unittest.TestCase): # Create some test data trade = create_trade(15.0, exchange.Exchange.BITTREX) - self.assertTrue(trade) + assert trade trade.close_rate = 0.07256061 trade.close_profit = 100.00 trade.close_date = datetime.utcnow() @@ -153,9 +153,9 @@ class TestTelegram(unittest.TestCase): 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]) + 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_5_start_handle(self): with patch.dict('freqtrade.main._CONF', self.conf): @@ -164,10 +164,10 @@ class TestTelegram(unittest.TestCase): init(self.conf, 'sqlite://') update_state(State.STOPPED) - self.assertEqual(get_state(), State.STOPPED) + assert get_state() == State.STOPPED _start(bot=MagicBot(), update=self.update) - self.assertEqual(get_state(), State.RUNNING) - self.assertEqual(msg_mock.call_count, 0) + assert get_state() == State.RUNNING + assert msg_mock.call_count == 0 def test_6_stop_handle(self): with patch.dict('freqtrade.main._CONF', self.conf): @@ -176,11 +176,11 @@ class TestTelegram(unittest.TestCase): init(self.conf, 'sqlite://') update_state(State.RUNNING) - self.assertEqual(get_state(), State.RUNNING) + assert 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]) + assert get_state() == State.STOPPED + assert msg_mock.call_count == 1 + assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] def setUp(self): self.update = Update(0) From 1eee0c91bf931c29b5a1f5388793c1a06ee49080 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 20:59:54 +0200 Subject: [PATCH 27/57] adapt README --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3dc649d06..9d4c457d8 100644 --- a/README.md +++ b/README.md @@ -65,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)). @@ -73,7 +74,7 @@ There is also an [article](https://www.sales4k.com/blockchain/high-frequency-tra #### Execute tests ``` -$ python -m unittest +$ python setup.py test ``` #### Docker From 805733350164f6dc98e9c554056ff82ec892131d Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 21:00:14 +0200 Subject: [PATCH 28/57] adapt Dockerfile for new project structure --- Dockerfile | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) 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"] From a4a1f7961a69349c49e48fa64672a13edf91878f Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 21:00:42 +0200 Subject: [PATCH 29/57] set executable bit --- bin/freqtrade | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/freqtrade diff --git a/bin/freqtrade b/bin/freqtrade old mode 100644 new mode 100755 From 3456ead83906859054904de90a76a2eb7b29224b Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 21:00:53 +0200 Subject: [PATCH 30/57] add numpy as required dep --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c6a1e399b..7ceb3b850 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ pandas==0.20.3 scikit-learn==0.19.0 scipy==0.19.1 jsonschema==2.6.0 +numpy==1.13.3 TA-Lib==0.4.10 # Required for plotting data From e42edd9de7b8ac33d28d721dad126a386a939310 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 30 Sep 2017 21:01:23 +0200 Subject: [PATCH 31/57] add required folders to MANIFEST --- MANIFEST.in | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index c41afdcf9..b018ee274 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,6 @@ include LICENSE include README.md -include config.json.example \ No newline at end of file +include config.json.example +include freqtrade/rpc/*.py +include freqtrade/tests/*.py +include freqtrade/tests/testdata/*.json \ No newline at end of file From 06ad311aa3e64bfe76967e3bdd050f1856d19a19 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 11:02:47 +0300 Subject: [PATCH 32/57] remove Test classes and use pytest fixtures --- freqtrade/tests/test_analyze.py | 52 +++--- freqtrade/tests/test_backtesting.py | 81 ++++---- freqtrade/tests/test_main.py | 139 +++++++------- freqtrade/tests/test_persistence.py | 38 ++-- freqtrade/tests/test_telegram.py | 280 ++++++++++++++-------------- 5 files changed, 288 insertions(+), 302 deletions(-) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 74e945da2..3f3c1610a 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -1,7 +1,7 @@ # pragma pylint: disable=missing-docstring -import unittest from unittest.mock import patch +import pytest import arrow from pandas import DataFrame @@ -19,34 +19,30 @@ RESULT_BITTREX = { ] } -class TestAnalyze(unittest.TestCase): - def setUp(self): - self.result = parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00')) +@pytest.fixture +def result(): + return parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00')) - def test_1_dataframe_has_correct_columns(self): - assert self.result.columns.tolist() == \ - ['close', 'high', 'low', 'open', 'date', 'volume'] +def test_1_dataframe_has_correct_columns(result): + assert result.columns.tolist() == \ + ['close', 'high', 'low', 'open', 'date', 'volume'] - def test_2_orders_by_date(self): - assert 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_2_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_3_populates_buy_trend(self): - dataframe = populate_buy_trend(populate_indicators(self.result)) - assert 'buy' in dataframe.columns - assert 'buy_price' in dataframe.columns +def test_3_populates_buy_trend(result): + dataframe = populate_buy_trend(populate_indicators(result)) + assert 'buy' in dataframe.columns + assert 'buy_price' in dataframe.columns - def test_4_returns_latest_buy_signal(self): - buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) - with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): - assert get_buy_signal('BTC-ETH') == True - buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) - with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): - assert get_buy_signal('BTC-ETH') == False - - -if __name__ == '__main__': - unittest.main() +def test_4_returns_latest_buy_signal(): + buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) + with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): + assert get_buy_signal('BTC-ETH') == True + buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) + with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): + assert get_buy_signal('BTC-ETH') == False diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index ff8f8a039..fe69104ff 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -2,9 +2,9 @@ import json import logging import os -import unittest from unittest.mock import patch +import pytest import arrow from pandas import DataFrame @@ -12,6 +12,7 @@ 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( @@ -21,9 +22,14 @@ def print_results(results): 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 = { +@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, @@ -33,43 +39,40 @@ class TestMain(unittest.TestCase): "stoploss": -0.40 } - @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('freqtrade.main._CONF', self.conf): - for pair in self.pairs: - with open('testdata/'+pair+'.json') as data_file: - data = json.load(data_file) +@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") +def test_backtest(conf, pairs): + trades = [] + with patch.dict('freqtrade.main._CONF', conf): + for pair in pairs: + with open('tests/testdata/'+pair+'.json') as data_file: + data = json.load(data_file) - with patch('freqtrade.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 + with patch('freqtrade.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) + trades.append((pair, current_profit, index2 - index)) + break + + labels = ['currency', 'profit', 'duration'] + results = DataFrame.from_records(trades, columns=labels) - print('====================== BACKTESTING REPORT ================================') + 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) + 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 index 73bb153de..bb075aee0 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,7 +1,7 @@ -import unittest +import copy from unittest.mock import patch, MagicMock, call -import copy +import pytest from jsonschema import validate from freqtrade import exchange @@ -11,8 +11,9 @@ from freqtrade.misc import CONF_SCHEMA from freqtrade.persistence import Trade -class TestMain(unittest.TestCase): - conf = { +@pytest.fixture +def conf(): + configuration = { "max_open_trades": 3, "stake_currency": "BTC", "stake_amount": 0.05, @@ -42,86 +43,80 @@ class TestMain(unittest.TestCase): "chat_id": "chat_id" } } + validate(configuration, CONF_SCHEMA) + return configuration - def test_1_create_trade(self): - with patch.dict('freqtrade.main._CONF', self.conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) as buy_signal: - with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): - with patch.multiple('freqtrade.main.exchange', - 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(self.conf['bittrex']['pair_whitelist']) - - init(self.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 == True - assert trade.open_date is not None - assert whitelist == self.conf['bittrex']['pair_whitelist'] - - buy_signal.assert_has_calls( - [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] - ) - - def test_2_handle_trade(self): - with patch.dict('freqtrade.main._CONF', self.conf): +def test_1_create_trade(conf): + with patch.dict('freqtrade.main._CONF', conf): + with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) as buy_signal: with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): with patch.multiple('freqtrade.main.exchange', get_ticker=MagicMock(return_value={ - 'bid': 0.17256061, - 'ask': 0.172661, - 'last': 0.17256061 + 'bid': 0.07256061, + 'ask': 0.072661, + 'last': 0.07256061 }), 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' + # Save state of current whitelist + whitelist = copy.deepcopy(conf['bittrex']['pair_whitelist']) - def test_3_close_trade(self): - with patch.dict('freqtrade.main._CONF', self.conf): - trade = Trade.query.filter(Trade.is_open.is_(True)).first() - assert trade + 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 == True + assert trade.open_date is not None + assert whitelist == conf['bittrex']['pair_whitelist'] - # Simulate that there is no open order - trade.open_order_id = None + buy_signal.assert_has_calls( + [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] + ) - closed = close_trade_if_fulfilled(trade) - assert closed - assert trade.is_open == False +def test_2_handle_trade(conf): + with patch.dict('freqtrade.main._CONF', conf): + with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): + with patch.multiple('freqtrade.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() + 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_balance_fully_ask_side(self): - with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}): - assert get_target_bid({'ask': 20, 'last': 10}) == 20 +def test_3_close_trade(conf): + with patch.dict('freqtrade.main._CONF', conf): + trade = Trade.query.filter(Trade.is_open.is_(True)).first() + assert trade - def test_balance_fully_last_side(self): - with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): - assert get_target_bid({'ask': 20, 'last': 10}) == 10 + # Simulate that there is no open order + trade.open_order_id = None - def test_balance_when_last_bigger_than_ask(self): - with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): - assert get_target_bid({'ask': 5, 'last': 10}) == 5 + closed = close_trade_if_fulfilled(trade) + assert closed + assert trade.is_open == False - @classmethod - def setUpClass(cls): - validate(cls.conf, CONF_SCHEMA) +def test_balance_fully_ask_side(): + with 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(): + with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): + assert get_target_bid({'ask': 20, 'last': 10}) == 10 -if __name__ == '__main__': - unittest.main() +def test_balance_when_last_bigger_than_ask(): + with 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 index 5b7c94989..eed6c1203 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,28 +1,22 @@ -import unittest from unittest.mock import patch from freqtrade.exchange import Exchange from freqtrade.persistence import Trade -class TestTrade(unittest.TestCase): - def test_1_exec_sell_order(self): - with patch('freqtrade.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) - assert profit == 100.0 - assert trade.close_rate == 1.0 - assert trade.close_profit == profit - assert trade.close_date is not None - - -if __name__ == '__main__': - unittest.main() +def test_1_exec_sell_order(): + with patch('freqtrade.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) + 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 index f86090d35..efc89ea25 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -1,7 +1,7 @@ -import unittest from datetime import datetime from unittest.mock import patch, MagicMock +import pytest from jsonschema import validate from telegram import Bot, Update, Message, Chat @@ -12,13 +12,9 @@ from freqtrade.persistence import Trade from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop -class MagicBot(MagicMock, Bot): - pass - - -class TestTelegram(unittest.TestCase): - - conf = { +@pytest.fixture +def conf(): + configuration = { "max_open_trades": 3, "stake_currency": "BTC", "stake_amount": 0.05, @@ -46,150 +42,152 @@ class TestTelegram(unittest.TestCase): }, "initial_state": "running" } + validate(configuration, CONF_SCHEMA) + return configuration - def test_1_status_handle(self): - with patch.dict('freqtrade.main._CONF', self.conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.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://') +@pytest.fixture +def update(): + _update = Update(0) + _update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0)) + return _update - # 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=self.update) - assert msg_mock.call_count == 2 - assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] +class MagicBot(MagicMock, Bot): + pass - def test_2_profit_handle(self): - with patch.dict('freqtrade.main._CONF', self.conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.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) - 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=self.update) - assert msg_mock.call_count == 2 - assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] - - def test_3_forcesell_handle(self): - with patch.dict('freqtrade.main._CONF', self.conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.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) - assert trade - Trade.session.add(trade) - Trade.session.flush() - - self.update.message.text = '/forcesell 1' - _forcesell(bot=MagicBot(), update=self.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_4_performance_handle(self): - with patch.dict('freqtrade.main._CONF', self.conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.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) - 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=self.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_5_start_handle(self): - with patch.dict('freqtrade.main._CONF', self.conf): +def test_1_status_handle(conf, update): + with patch.dict('freqtrade.main._CONF', conf): + with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): - init(self.conf, 'sqlite://') + with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, + 'last': 0.07256061 + }), + buy=MagicMock(return_value='mocked_order_id')): + init(conf, 'sqlite://') - update_state(State.STOPPED) - assert get_state() == State.STOPPED - _start(bot=MagicBot(), update=self.update) - assert get_state() == State.RUNNING - assert msg_mock.call_count == 0 + # Create some test data + trade = create_trade(15.0, exchange.Exchange.BITTREX) + assert trade + Trade.session.add(trade) + Trade.session.flush() - def test_6_stop_handle(self): - with patch.dict('freqtrade.main._CONF', self.conf): + _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_2_profit_handle(conf, update): + with patch.dict('freqtrade.main._CONF', conf): + with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock): - init(self.conf, 'sqlite://') + with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, + 'last': 0.07256061 + }), + buy=MagicMock(return_value='mocked_order_id')): + init(conf, 'sqlite://') - update_state(State.RUNNING) - assert get_state() == State.RUNNING - _stop(bot=MagicBot(), update=self.update) - assert get_state() == State.STOPPED - assert msg_mock.call_count == 1 - assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] + # 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() - def setUp(self): - self.update = Update(0) - self.update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0)) + _profit(bot=MagicBot(), update=update) + assert msg_mock.call_count == 2 + assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] - @classmethod - def setUpClass(cls): - validate(cls.conf, CONF_SCHEMA) +def test_3_forcesell_handle(conf, update): + with patch.dict('freqtrade.main._CONF', conf): + with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): + msg_mock = MagicMock() + with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', + 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_4_performance_handle(conf, update): + with patch.dict('freqtrade.main._CONF', conf): + with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): + msg_mock = MagicMock() + with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): + with patch.multiple('freqtrade.main.exchange', + 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_5_start_handle(conf, update): + with patch.dict('freqtrade.main._CONF', conf): + msg_mock = MagicMock() + with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): + 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_6_stop_handle(conf, update): + with patch.dict('freqtrade.main._CONF', conf): + msg_mock = MagicMock() + with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): + 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] -if __name__ == '__main__': - unittest.main() From 9cca42e371ef2ca3d9f1c3b22a6135a2a14a697c Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 11:06:40 +0300 Subject: [PATCH 33/57] add pytest to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c6a1e399b..2cfc05081 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ scikit-learn==0.19.0 scipy==0.19.1 jsonschema==2.6.0 TA-Lib==0.4.10 +pytest==3.2.2 # Required for plotting data #matplotlib==2.0.2 From 616d5b61cc854b126f1398fc91ecb83a491e76dc Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 11:11:20 +0300 Subject: [PATCH 34/57] remove numbers from test method names --- freqtrade/tests/test_analyze.py | 8 ++++---- freqtrade/tests/test_main.py | 6 +++--- freqtrade/tests/test_persistence.py | 2 +- freqtrade/tests/test_telegram.py | 13 ++++++------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 3f3c1610a..413390a31 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -23,23 +23,23 @@ RESULT_BITTREX = { def result(): return parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00')) -def test_1_dataframe_has_correct_columns(result): +def test_dataframe_has_correct_columns(result): assert result.columns.tolist() == \ ['close', 'high', 'low', 'open', 'date', 'volume'] -def test_2_orders_by_date(result): +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_3_populates_buy_trend(result): +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_4_returns_latest_buy_signal(): +def test_returns_latest_buy_signal(): buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): assert get_buy_signal('BTC-ETH') == True diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index bb075aee0..7e2b41932 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -46,7 +46,7 @@ def conf(): validate(configuration, CONF_SCHEMA) return configuration -def test_1_create_trade(conf): +def test_create_trade(conf): with patch.dict('freqtrade.main._CONF', conf): with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) as buy_signal: with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): @@ -79,7 +79,7 @@ def test_1_create_trade(conf): [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] ) -def test_2_handle_trade(conf): +def test_handle_trade(conf): with patch.dict('freqtrade.main._CONF', conf): with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): with patch.multiple('freqtrade.main.exchange', @@ -97,7 +97,7 @@ def test_2_handle_trade(conf): assert trade.close_date is not None assert trade.open_order_id == 'dry_run' -def test_3_close_trade(conf): +def test_close_trade(conf): with patch.dict('freqtrade.main._CONF', conf): trade = Trade.query.filter(Trade.is_open.is_(True)).first() assert trade diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index eed6c1203..19ff4df10 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -4,7 +4,7 @@ from freqtrade.exchange import Exchange from freqtrade.persistence import Trade -def test_1_exec_sell_order(): +def test_exec_sell_order(): with patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id') as api_mock: trade = Trade( pair='BTC_ETH', diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index efc89ea25..ef7b76868 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -56,7 +56,7 @@ class MagicBot(MagicMock, Bot): pass -def test_1_status_handle(conf, update): +def test_status_handle(conf, update): with patch.dict('freqtrade.main._CONF', conf): with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): msg_mock = MagicMock() @@ -80,7 +80,7 @@ def test_1_status_handle(conf, update): assert msg_mock.call_count == 2 assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] -def test_2_profit_handle(conf, update): +def test_profit_handle(conf, update): with patch.dict('freqtrade.main._CONF', conf): with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): msg_mock = MagicMock() @@ -109,7 +109,7 @@ def test_2_profit_handle(conf, update): assert msg_mock.call_count == 2 assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] -def test_3_forcesell_handle(conf, update): +def test_forcesell_handle(conf, update): with patch.dict('freqtrade.main._CONF', conf): with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): msg_mock = MagicMock() @@ -136,7 +136,7 @@ def test_3_forcesell_handle(conf, update): 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_4_performance_handle(conf, update): +def test_performance_handle(conf, update): with patch.dict('freqtrade.main._CONF', conf): with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): msg_mock = MagicMock() @@ -166,7 +166,7 @@ def test_4_performance_handle(conf, update): 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_5_start_handle(conf, update): +def test_start_handle(conf, update): with patch.dict('freqtrade.main._CONF', conf): msg_mock = MagicMock() with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): @@ -178,7 +178,7 @@ def test_5_start_handle(conf, update): assert get_state() == State.RUNNING assert msg_mock.call_count == 0 -def test_6_stop_handle(conf, update): +def test_stop_handle(conf, update): with patch.dict('freqtrade.main._CONF', conf): msg_mock = MagicMock() with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): @@ -190,4 +190,3 @@ def test_6_stop_handle(conf, update): assert get_state() == State.STOPPED assert msg_mock.call_count == 1 assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] - From e14cc2e4f6d6d9ca31f95018bc1a4cfb220f0849 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 1 Oct 2017 14:06:18 +0200 Subject: [PATCH 35/57] add setup.cfg to force pytest --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..6a53971b3 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[aliases] +test=pytest + From 378b5a3b14ff2a13fc996e1f20245195efcb0812 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 1 Oct 2017 14:07:09 +0200 Subject: [PATCH 36/57] fix indentation --- config.json.example | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/config.json.example b/config.json.example index 584fb019d..c2f6668d4 100644 --- a/config.json.example +++ b/config.json.example @@ -3,14 +3,14 @@ "stake_currency": "BTC", "stake_amount": 0.05, "dry_run": false, - "minimal_roi": { - "60": 0.0, - "40": 0.01, - "20": 0.02, - "0": 0.03 - }, - "stoploss": -0.40, - "bid_strategy": { + "minimal_roi": { + "60": 0.0, + "40": 0.01, + "20": 0.02, + "0": 0.03 + }, + "stoploss": -0.40, + "bid_strategy": { "ask_last_balance": 0.0 }, "bittrex": { From add6c875d6316d7d5c3019eb30ea09b427021b1a Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 15:24:27 +0300 Subject: [PATCH 37/57] add pytest-mock to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 2cfc05081..0d360c10b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ scipy==0.19.1 jsonschema==2.6.0 TA-Lib==0.4.10 pytest==3.2.2 +pytest-mock==1.6.3 # Required for plotting data #matplotlib==2.0.2 From ff145b630608c025f956a63fa9a42988fab22ec4 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 15:25:10 +0300 Subject: [PATCH 38/57] use mocker for mocking to get rid of deep nesting --- freqtrade/tests/test_analyze.py | 13 +- freqtrade/tests/test_backtesting.py | 43 +++-- freqtrade/tests/test_main.py | 132 +++++++-------- freqtrade/tests/test_persistence.py | 34 ++-- freqtrade/tests/test_telegram.py | 238 ++++++++++++++-------------- 5 files changed, 228 insertions(+), 232 deletions(-) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 413390a31..48c46822f 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -1,6 +1,4 @@ # pragma pylint: disable=missing-docstring -from unittest.mock import patch - import pytest import arrow from pandas import DataFrame @@ -39,10 +37,11 @@ def test_populates_buy_trend(result): assert 'buy' in dataframe.columns assert 'buy_price' in dataframe.columns -def test_returns_latest_buy_signal(): +def test_returns_latest_buy_signal(mocker): buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}]) - with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): - assert get_buy_signal('BTC-ETH') == True + mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) + assert get_buy_signal('BTC-ETH') == True + buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) - with patch('freqtrade.analyze.analyze_ticker', return_value=buydf): - assert get_buy_signal('BTC-ETH') == False + mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) + assert get_buy_signal('BTC-ETH') == False diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index fe69104ff..dcb572157 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -2,7 +2,6 @@ import json import logging import os -from unittest.mock import patch import pytest import arrow @@ -41,30 +40,30 @@ def conf(): @pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set") -def test_backtest(conf, pairs): +def test_backtest(conf, pairs, mocker): trades = [] - with patch.dict('freqtrade.main._CONF', conf): - for pair in pairs: - with open('tests/testdata/'+pair+'.json') as data_file: - data = json.load(data_file) + mocker.patch.dict('freqtrade.main._CONF', conf) + for pair in pairs: + with open('tests/testdata/'+pair+'.json') as data_file: + data = json.load(data_file) - with patch('freqtrade.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 + 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 + trades.append((pair, current_profit, index2 - index)) + break labels = ['currency', 'profit', 'duration'] results = DataFrame.from_records(trades, columns=labels) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 7e2b41932..8c881c9c8 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,5 +1,5 @@ import copy -from unittest.mock import patch, MagicMock, call +from unittest.mock import MagicMock, call import pytest from jsonschema import validate @@ -46,77 +46,77 @@ def conf(): validate(configuration, CONF_SCHEMA) return configuration -def test_create_trade(conf): - with patch.dict('freqtrade.main._CONF', conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) as buy_signal: - with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): - with patch.multiple('freqtrade.main.exchange', - 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']) +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', + 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 == True - assert trade.open_date is not None - assert whitelist == 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 == True + 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')] - ) + buy_signal.assert_has_calls( + [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] + ) -def test_handle_trade(conf): - with patch.dict('freqtrade.main._CONF', conf): - with patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()): - with patch.multiple('freqtrade.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() - 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_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', + 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): - with patch.dict('freqtrade.main._CONF', conf): - trade = Trade.query.filter(Trade.is_open.is_(True)).first() - assert trade +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 + # Simulate that there is no open order + trade.open_order_id = None - closed = close_trade_if_fulfilled(trade) - assert closed - assert trade.is_open == False + closed = close_trade_if_fulfilled(trade) + assert closed + assert trade.is_open == False -def test_balance_fully_ask_side(): - with 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_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(): - with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): - assert get_target_bid({'ask': 20, 'last': 10}) == 10 +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(): - with patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}): - assert get_target_bid({'ask': 5, 'last': 10}) == 5 +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 index 19ff4df10..6015fe858 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,22 +1,20 @@ -from unittest.mock import patch - from freqtrade.exchange import Exchange from freqtrade.persistence import Trade -def test_exec_sell_order(): - with patch('freqtrade.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) - assert profit == 100.0 - assert trade.close_rate == 1.0 - assert trade.close_profit == profit - assert trade.close_date is not None +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 index ef7b76868..c054b8994 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -1,5 +1,5 @@ from datetime import datetime -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock import pytest from jsonschema import validate @@ -56,137 +56,137 @@ class MagicBot(MagicMock, Bot): pass -def test_status_handle(conf, update): - with patch.dict('freqtrade.main._CONF', conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.main.exchange', - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 - }), - buy=MagicMock(return_value='mocked_order_id')): - init(conf, 'sqlite://') +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', + 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() + # 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] + _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): - with patch.dict('freqtrade.main._CONF', conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.main.exchange', - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 - }), - buy=MagicMock(return_value='mocked_order_id')): - init(conf, 'sqlite://') +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', + 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() + # 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] + _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): - with patch.dict('freqtrade.main._CONF', conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.main.exchange', - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 - }), - buy=MagicMock(return_value='mocked_order_id')): - init(conf, 'sqlite://') +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', + 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() + # 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) + 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] + 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): - with patch.dict('freqtrade.main._CONF', conf): - with patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): - with patch.multiple('freqtrade.main.exchange', - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 - }), - buy=MagicMock(return_value='mocked_order_id')): - init(conf, 'sqlite://') +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', + 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() + # 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] + _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): - with patch.dict('freqtrade.main._CONF', conf): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): - init(conf, 'sqlite://') +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) + 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 + 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): - with patch.dict('freqtrade.main._CONF', conf): - msg_mock = MagicMock() - with patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock): - init(conf, 'sqlite://') +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) + 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] + 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] From 5551c9ec3bc0d22a8967adffcd35950cfa7c83da Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 15:40:40 +0300 Subject: [PATCH 39/57] add pragmas to disable pylint warnings for missing docstrings in test files --- freqtrade/tests/test_main.py | 1 + freqtrade/tests/test_persistence.py | 2 +- freqtrade/tests/test_telegram.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 8c881c9c8..38f60af43 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,3 +1,4 @@ +# pragma pylint: disable=missing-docstring import copy from unittest.mock import MagicMock, call diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 6015fe858..376825c72 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,7 +1,7 @@ +# 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( diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index c054b8994..cb67c8c79 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -1,3 +1,4 @@ +# pragma pylint: disable=missing-docstring from datetime import datetime from unittest.mock import MagicMock From 5537f0bf5b436016d582b7029940392ecadc1201 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 15:45:21 +0300 Subject: [PATCH 40/57] simplify unnecessary == True and == False assertions --- freqtrade/tests/test_analyze.py | 4 ++-- freqtrade/tests/test_main.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 48c46822f..e716eb3ad 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -40,8 +40,8 @@ def test_populates_buy_trend(result): 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') == True + assert get_buy_signal('BTC-ETH') buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}]) mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf) - assert get_buy_signal('BTC-ETH') == False + assert not get_buy_signal('BTC-ETH') diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 38f60af43..eb87de1e3 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -72,7 +72,7 @@ def test_create_trade(conf, mocker): assert trade.exchange == exchange.Exchange.BITTREX assert trade.amount == 206.43811673387373 assert trade.stake_amount == 15.0 - assert trade.is_open == True + assert trade.is_open assert trade.open_date is not None assert whitelist == conf['bittrex']['pair_whitelist'] @@ -108,7 +108,7 @@ def test_close_trade(conf, mocker): closed = close_trade_if_fulfilled(trade) assert closed - assert trade.is_open == False + 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}}) From 17e8bbacc3ee72b3bd51f58afe288aacd88ee149 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 16:17:27 +0300 Subject: [PATCH 41/57] add pytest-mock to setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2d8d40990..2cd551cb2 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup(name='freqtrade', packages=['freqtrade'], scripts=['bin/freqtrade'], setup_requires=['pytest-runner'], - tests_require=['pytest'], + tests_require=['pytest', 'pytest-mock'], install_requires=[ 'python-bittrex==0.1.3', 'SQLAlchemy==1.1.13', From 02673b94dd875d626c9b4c7818dd51fe70046f35 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 17:04:38 +0300 Subject: [PATCH 42/57] use explicit package name for pytest running --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 6a53971b3..669531eec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,4 @@ [aliases] -test=pytest +test=pytest --pyargs freqtrade + From ea62c49c3a77e1d5237463d579ae1ecb51005354 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sun, 1 Oct 2017 17:19:14 +0300 Subject: [PATCH 43/57] fix passing parameters to pytest --- setup.cfg | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 669531eec..c5d8a376a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,5 @@ [aliases] -test=pytest --pyargs freqtrade - +test=pytest +[tool:pytest] +addopts = --pyargs freqtrade From f44ab2f44b9d1f8cf9602f80961a9d725ad8ea4f Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 1 Oct 2017 23:28:09 +0200 Subject: [PATCH 44/57] patch missing http calls --- freqtrade/exchange.py | 15 ++++++++++++--- freqtrade/tests/test_main.py | 3 +++ freqtrade/tests/test_telegram.py | 6 ++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange.py b/freqtrade/exchange.py index 2b0947311..96bc13159 100644 --- a/freqtrade/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/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index eb87de1e3..63da42e37 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -6,6 +6,7 @@ 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 @@ -52,6 +53,7 @@ def test_create_trade(conf, mocker): 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, @@ -84,6 +86,7 @@ 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, diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index cb67c8c79..e01c5060a 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -63,6 +63,7 @@ def test_status_handle(conf, update, mocker): 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, @@ -87,6 +88,7 @@ def test_profit_handle(conf, update, mocker): 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, @@ -116,6 +118,7 @@ def test_forcesell_handle(conf, update, mocker): 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, @@ -143,6 +146,7 @@ def test_performance_handle(conf, update, mocker): 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, @@ -171,6 +175,7 @@ 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) @@ -183,6 +188,7 @@ 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) From b2522b8dbcf6194fdd6b9a27bbc85eb9c30692f8 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 2 Oct 2017 19:17:54 +0300 Subject: [PATCH 45/57] add pytest-cov dependency --- requirements.txt | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b070fe0ed..b7bf943ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ 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 diff --git a/setup.py b/setup.py index 2cd551cb2..e89fd2eee 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ setup(name='freqtrade', packages=['freqtrade'], scripts=['bin/freqtrade'], setup_requires=['pytest-runner'], - tests_require=['pytest', 'pytest-mock'], + tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ 'python-bittrex==0.1.3', 'SQLAlchemy==1.1.13', From 8500032bff2778003998efbe13c70ee0e3d82184 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 2 Oct 2017 19:27:18 +0300 Subject: [PATCH 46/57] add coverage config file to omit test files from coverage report --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .coveragerc 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/* From 27b2624a6784a69d72e1a38a0da00bea83b47009 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 2 Oct 2017 19:27:50 +0300 Subject: [PATCH 47/57] let pytest do coverage --- .travis.yml | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index aa1e01d31..a2a69c9e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ install: - pip install coveralls - pip install -r requirements.txt script: -- coverage run --source=freqtrade setup.py test +- python setup.py test after_success: - coveralls notifications: diff --git a/setup.cfg b/setup.cfg index c5d8a376a..ea59030de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,4 +2,4 @@ test=pytest [tool:pytest] -addopts = --pyargs freqtrade +addopts = --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ From 3506e3ceec2bc5c7669cf136ae8c19d2b3803853 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Mon, 2 Oct 2017 20:17:14 +0300 Subject: [PATCH 48/57] try directly invoking pytest for fixing coveralls issue --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a2a69c9e3..461324f45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ install: - pip install coveralls - pip install -r requirements.txt script: -- python setup.py test +- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ after_success: - coveralls notifications: From e0896fdd7bb7ef3492d32f1477dfab288494dbd7 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Fri, 6 Oct 2017 10:50:48 +0300 Subject: [PATCH 49/57] fix incorrect backtest testdata path --- freqtrade/tests/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index dcb572157..293b752b4 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -44,7 +44,7 @@ def test_backtest(conf, pairs, mocker): trades = [] mocker.patch.dict('freqtrade.main._CONF', conf) for pair in pairs: - with open('tests/testdata/'+pair+'.json') as data_file: + with open('freqtrade/tests/testdata/'+pair+'.json') as data_file: data = json.load(data_file) mocker.patch('freqtrade.analyze.get_ticker', return_value=data) From b9eb2662361919bd45728c3029c438fd62c8051a Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Fri, 6 Oct 2017 12:22:04 +0200 Subject: [PATCH 50/57] Exchange refactoring --- config.json.example | 11 +- freqtrade/analyze.py | 44 +++----- freqtrade/exchange.py | 154 +++++++++------------------- freqtrade/exchanges/__init__.py | 127 +++++++++++++++++++++++ freqtrade/exchanges/bittrex.py | 120 ++++++++++++++++++++++ freqtrade/main.py | 23 +++-- freqtrade/misc.py | 7 +- freqtrade/persistence.py | 2 +- freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/test_backtesting.py | 4 +- freqtrade/tests/test_main.py | 14 +-- freqtrade/tests/test_persistence.py | 4 +- freqtrade/tests/test_telegram.py | 12 +-- 13 files changed, 350 insertions(+), 174 deletions(-) create mode 100644 freqtrade/exchanges/__init__.py create mode 100644 freqtrade/exchanges/bittrex.py diff --git a/config.json.example b/config.json.example index c2f6668d4..8f54a6a37 100644 --- a/config.json.example +++ b/config.json.example @@ -4,16 +4,17 @@ "stake_amount": 0.05, "dry_run": false, "minimal_roi": { - "60": 0.0, - "40": 0.01, - "20": 0.02, - "0": 0.03 + "60": 0.0, + "40": 0.01, + "20": 0.02, + "0": 0.03 }, "stoploss": -0.40, "bid_strategy": { "ask_last_balance": 0.0 }, - "bittrex": { + "exchange": { + "name": "bittrex", "enabled": true, "key": "key", "secret": "secret", diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index d8b86d945..159e1d137 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -1,36 +1,18 @@ +import logging import time from datetime import timedelta -import logging -import arrow -import requests -from pandas import DataFrame -import talib.abstract as ta +import arrow +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.exchange import get_ticker_history logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) -def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict: - """ - Request ticker data from Bittrex for a given currency pair - """ - url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks' - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', - } - params = { - 'marketName': pair.replace('_', '-'), - 'tickInterval': 'fiveMin', - '_': minimum_date.timestamp * 1000 - } - data = requests.get(url, params=params, headers=headers).json() - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return data - - def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame: """ Analyses the trend for the given pair @@ -43,6 +25,7 @@ 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 @@ -87,17 +70,18 @@ def analyze_ticker(pair: str) -> DataFrame: :return DataFrame with ticker data and indicator data """ minimum_date = arrow.utcnow().shift(hours=-24) - data = get_ticker(pair, minimum_date) + data = get_ticker_history(pair, minimum_date) dataframe = parse_ticker_dataframe(data['result'], minimum_date) if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return dataframe - + dataframe = populate_indicators(dataframe) dataframe = populate_buy_trend(dataframe) return dataframe + def get_buy_signal(pair: str) -> bool: """ Calculates a buy signal based several technical analysis indicators @@ -144,9 +128,9 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None: ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy') ax1.legend() -# ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') + # ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI') -# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values)) + # 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 @@ -160,7 +144,7 @@ if __name__ == '__main__': # Install PYQT5==5.9 manually if you want to test this helper function while True: test_pair = 'BTC_ETH' - #for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: - # get_buy_signal(pair) + # for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: + # get_buy_signal(pair) plot_dataframe(analyze_ticker(test_pair), test_pair) time.sleep(60) diff --git a/freqtrade/exchange.py b/freqtrade/exchange.py index 96bc13159..0c1dd3ad9 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange.py @@ -2,18 +2,23 @@ import enum import logging from typing import List -from bittrex.bittrex import Bittrex +import arrow + +from freqtrade.exchanges import Exchange +from freqtrade.exchanges.bittrex import Bittrex logger = logging.getLogger(__name__) # Current selected exchange -EXCHANGE = None -_API = None -_CONF = {} +EXCHANGE: Exchange = None +_CONF: dict = {} -class Exchange(enum.Enum): - BITTREX = 1 +class Exchanges(enum.Enum): + """ + Maps supported exchange names to correspondent classes. + """ + BITTREX = Bittrex def init(config: dict) -> None: @@ -24,22 +29,32 @@ def init(config: dict) -> None: :param config: config to use :return: None """ - global _API, EXCHANGE + global _CONF, EXCHANGE _CONF.update(config) if config['dry_run']: logger.info('Instance is running with dry_run enabled') - use_bittrex = config.get('bittrex', {}).get('enabled', False) - if use_bittrex: - EXCHANGE = Exchange.BITTREX - _API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret']) - else: - raise RuntimeError('No exchange specified. Aborting!') + exchange_config = config['exchange'] + name = exchange_config['name'] + + # Find matching class for the given exchange name + exchange_class = None + for exchange in Exchanges: + if name.upper() == exchange.name: + exchange_class = exchange.value + break + if not exchange_class: + raise RuntimeError('Exchange {} is not supported'.format(name)) + + if not exchange_config.get('enabled', False): + raise RuntimeError('Exchange {} is disabled'.format(name)) + + EXCHANGE = exchange_class(exchange_config) # Check if all pairs are available - validate_pairs(config[EXCHANGE.name.lower()]['pair_whitelist']) + validate_pairs(config['exchange']['pair_whitelist']) def validate_pairs(pairs: List[str]) -> None: @@ -49,131 +64,58 @@ def validate_pairs(pairs: List[str]) -> None: :param pairs: list of pairs :return: None """ - markets = get_markets() + markets = EXCHANGE.get_markets() for pair in pairs: if pair not in markets: raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower())) def buy(pair: str, rate: float, amount: float) -> str: - """ - Places a limit buy order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to purchase - :return: order_id of the placed buy order - """ if _CONF['dry_run']: return 'dry_run' - elif EXCHANGE == Exchange.BITTREX: - data = _API.buy_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return data['result']['uuid'] + + return EXCHANGE.buy(pair, rate, amount) def sell(pair: str, rate: float, amount: float) -> str: - """ - Places a limit sell order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to sell - :return: None - """ if _CONF['dry_run']: return 'dry_run' - elif EXCHANGE == Exchange.BITTREX: - data = _API.sell_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return data['result']['uuid'] + + return EXCHANGE.sell(pair, rate, amount) def get_balance(currency: str) -> float: - """ - Get account balance. - :param currency: currency as str, format: BTC - :return: float - """ if _CONF['dry_run']: return 999.9 - elif EXCHANGE == Exchange.BITTREX: - data = _API.get_balance(currency) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return float(data['result']['Balance'] or 0.0) + + return EXCHANGE.get_balance(currency) def get_ticker(pair: str) -> dict: - """ - Get Ticker for given pair. - :param pair: Pair as str, format: BTC_ETC - :return: dict - """ - if EXCHANGE == Exchange.BITTREX: - data = _API.get_ticker(pair.replace('_', '-')) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return { - 'bid': float(data['result']['Bid']), - 'ask': float(data['result']['Ask']), - 'last': float(data['result']['Last']), - } + return EXCHANGE.get_ticker(pair) + + +def get_ticker_history(pair: str, minimum_date: arrow.Arrow): + return EXCHANGE.get_ticker_history(pair, minimum_date) def cancel_order(order_id: str) -> None: - """ - Cancel order for given order_id - :param order_id: id as str - :return: None - """ if _CONF['dry_run']: - pass - elif EXCHANGE == Exchange.BITTREX: - data = _API.cancel(order_id) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) + return + + return EXCHANGE.cancel_order(order_id) def get_open_orders(pair: str) -> List[dict]: - """ - Get all open orders for given pair. - :param pair: Pair as str, format: BTC_ETC - :return: list of dicts - """ if _CONF['dry_run']: return [] - elif EXCHANGE == Exchange.BITTREX: - data = _API.get_open_orders(pair.replace('_', '-')) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return [{ - 'id': entry['OrderUuid'], - 'type': entry['OrderType'], - 'opened': entry['Opened'], - 'rate': entry['PricePerUnit'], - 'amount': entry['Quantity'], - 'remaining': entry['QuantityRemaining'], - } for entry in data['result']] + + return EXCHANGE.get_open_orders(pair) def get_pair_detail_url(pair: str) -> str: - """ - Returns the market detail url for the given pair - :param pair: pair as str, format: BTC_ANT - :return: url as str - """ - if EXCHANGE == Exchange.BITTREX: - return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-')) + return EXCHANGE.get_pair_detail_url(pair) def get_markets() -> List[str]: - """ - Returns all available markets - :return: list of all available pairs - """ - if EXCHANGE == Exchange. BITTREX: - data = _API.get_markets() - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return [m['MarketName'].replace('-', '_') for m in data['result']] + return EXCHANGE.get_markets() diff --git a/freqtrade/exchanges/__init__.py b/freqtrade/exchanges/__init__.py new file mode 100644 index 000000000..114ac9a6f --- /dev/null +++ b/freqtrade/exchanges/__init__.py @@ -0,0 +1,127 @@ +from abc import ABC, abstractmethod +from typing import List, Optional + +import arrow + + +class Exchange(ABC): + @property + def name(self) -> str: + """ + Name of the exchange. + :return: str representation of the class name + """ + return self.__class__.__name__ + + @property + @abstractmethod + def sleep_time(self) -> float: + """ + Sleep time in seconds for the main loop to avoid API rate limits. + :return: float + """ + + @abstractmethod + def buy(self, pair: str, rate: float, amount: float) -> str: + """ + Places a limit buy order. + :param pair: Pair as str, format: BTC_ETH + :param rate: Rate limit for order + :param amount: The amount to purchase + :return: order_id of the placed buy order + """ + + @abstractmethod + def sell(self, pair: str, rate: float, amount: float) -> str: + """ + Places a limit sell order. + :param pair: Pair as str, format: BTC_ETH + :param rate: Rate limit for order + :param amount: The amount to sell + :return: order_id of the placed sell order + """ + + @abstractmethod + def get_balance(self, currency: str) -> float: + """ + Gets account balance. + :param currency: Currency as str, format: BTC + :return: float + """ + + @abstractmethod + def get_ticker(self, pair: str) -> dict: + """ + Gets ticker for given pair. + :param pair: Pair as str, format: BTC_ETC + :return: dict, format: { + 'bid': float, + 'ask': float, + 'last': float + } + """ + + @abstractmethod + def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None) -> dict: + """ + Gets ticker history for given pair. + :param pair: Pair as str, format: BTC_ETC + :param minimum_date: Minimum date (optional) + :return: dict, format: { + 'success': bool, + 'message': str, + 'result': [ + { + 'O': float, (Open) + 'H': float, (High) + 'L': float, (Low) + 'C': float, (Close) + 'V': float, (Volume) + 'T': datetime, (Time) + 'BV': float, (Base Volume) + }, + ... + ] + } + """ + + @abstractmethod + def cancel_order(self, order_id: str) -> None: + """ + Cancels order for given order_id. + :param order_id: ID as str + :return: None + """ + + @abstractmethod + def get_open_orders(self, pair: str) -> List[dict]: + """ + Gets all open orders for given pair. + :param pair: Pair as str, format: BTC_ETC + :return: List of dicts, format: [ + { + 'id': str, + 'type': str, + 'opened': datetime, + 'rate': float, + 'amount': float, + 'remaining': int, + }, + ... + ] + """ + + @abstractmethod + def get_pair_detail_url(self, pair: str) -> str: + """ + Returns the market detail url for the given pair. + :param pair: Pair as str, format: BTC_ETC + :return: URL as str + """ + + @abstractmethod + def get_markets(self) -> List[str]: + """ + Returns all available markets. + :return: List of all available pairs + """ diff --git a/freqtrade/exchanges/bittrex.py b/freqtrade/exchanges/bittrex.py new file mode 100644 index 000000000..3b5cd4330 --- /dev/null +++ b/freqtrade/exchanges/bittrex.py @@ -0,0 +1,120 @@ +import logging +from typing import List, Optional + +import arrow +import requests +from bittrex.bittrex import Bittrex as _Bittrex + +from freqtrade.exchanges import Exchange + +logger = logging.getLogger(__name__) + +_API: _Bittrex = None +_EXCHANGE_CONF: dict = {} + + +class Bittrex(Exchange): + """ + Bittrex API wrapper. + """ + # Base URL and API endpoints + BASE_URL: str = 'https://www.bittrex.com' + TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks' + PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index' + # Ticker inveral + TICKER_INTERVAL: str = 'fiveMin' + # Sleep time to avoid rate limits, used in the main loop + SLEEP_TIME: float = 25 + + @property + def name(self) -> str: + return self.__class__.__name__ + + @property + def sleep_time(self) -> float: + return self.SLEEP_TIME + + def __init__(self, config: dict) -> None: + global _API, _EXCHANGE_CONF + + _EXCHANGE_CONF.update(config) + _API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret']) + + # Check if all pairs are available + markets = self.get_markets() + exchange_name = self.name + for pair in _EXCHANGE_CONF['pair_whitelist']: + if pair not in markets: + raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name)) + + def buy(self, pair: str, rate: float, amount: float) -> str: + data = _API.buy_limit(pair.replace('_', '-'), amount, rate) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return data['result']['uuid'] + + def sell(self, pair: str, rate: float, amount: float) -> str: + data = _API.sell_limit(pair.replace('_', '-'), amount, rate) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return data['result']['uuid'] + + def get_balance(self, currency: str) -> float: + data = _API.get_balance(currency) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return float(data['result']['Balance'] or 0.0) + + def get_ticker(self, pair: str) -> dict: + data = _API.get_ticker(pair.replace('_', '-')) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return { + 'bid': float(data['result']['Bid']), + 'ask': float(data['result']['Ask']), + 'last': float(data['result']['Last']), + } + + def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None): + url = self.TICKER_METHOD + headers = { + # TODO: Set as global setting + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36' + } + params = { + 'marketName': pair.replace('_', '-'), + 'tickInterval': self.TICKER_INTERVAL, + # TODO: Timestamp has no effect on API response + '_': minimum_date.timestamp * 1000 + } + data = requests.get(url, params=params, headers=headers).json() + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return data + + def cancel_order(self, order_id: str) -> None: + data = _API.cancel(order_id) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + + def get_open_orders(self, pair: str) -> List[dict]: + data = _API.get_open_orders(pair.replace('_', '-')) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return [{ + 'id': entry['OrderUuid'], + 'type': entry['OrderType'], + 'opened': entry['Opened'], + 'rate': entry['PricePerUnit'], + 'amount': entry['Quantity'], + 'remaining': entry['QuantityRemaining'], + } for entry in data['result']] + + def get_pair_detail_url(self, pair: str) -> str: + return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) + + def get_markets(self) -> List[str]: + data = _API.get_markets() + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return [m['MarketName'].replace('-', '_') for m in data['result']] diff --git a/freqtrade/main.py b/freqtrade/main.py index 518c82a6d..1d712007c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import copy import json import logging import time @@ -6,12 +7,11 @@ import traceback from datetime import datetime from typing import Dict, Optional -import copy from jsonschema import validate -from freqtrade import exchange, persistence, __version__ +from freqtrade import __version__, exchange, persistence from freqtrade.analyze import get_buy_signal -from freqtrade.misc import State, update_state, get_state, CONF_SCHEMA +from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state from freqtrade.persistence import Trade from freqtrade.rpc import telegram @@ -34,7 +34,7 @@ def _process() -> None: if len(trades) < _CONF['max_open_trades']: try: # Create entity and execute trade - trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE) + trade = create_trade(float(_CONF['stake_amount'])) if trade: Trade.session.add(trade) else: @@ -91,7 +91,7 @@ def execute_sell(trade: Trade, current_rate: float) -> None: balance = exchange.get_balance(currency) profit = trade.exec_sell_order(current_rate, balance) message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( - trade.exchange.name, + trade.exchange, trade.pair.replace('_', '/'), exchange.get_pair_detail_url(trade.pair), trade.close_rate, @@ -142,6 +142,7 @@ def handle_trade(trade: Trade) -> None: except ValueError: logger.exception('Unable to handle open order') + def get_target_bid(ticker: Dict[str, float]) -> float: """ Calculates bid target between current ask price and last price """ if ticker['ask'] < ticker['last']: @@ -150,15 +151,15 @@ def get_target_bid(ticker: Dict[str, float]) -> float: return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) -def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]: +def create_trade(stake_amount: float) -> Optional[Trade]: """ Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created :param stake_amount: amount of btc to spend - :param _exchange: exchange to use + :param _exchange: """ logger.info('Creating new trade with stake_amount: %f ...', stake_amount) - whitelist = copy.deepcopy(_CONF[_exchange.name.lower()]['pair_whitelist']) + whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) # Check if stake_amount is fulfilled if exchange.get_balance(_CONF['stake_currency']) < stake_amount: raise ValueError( @@ -187,7 +188,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[ # Create trade entity and return message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format( - _exchange.name, + exchange.EXCHANGE.name.upper(), pair.replace('_', '/'), exchange.get_pair_detail_url(pair), open_rate @@ -199,7 +200,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[ open_rate=open_rate, open_date=datetime.utcnow(), amount=amount, - exchange=_exchange, + exchange=exchange.EXCHANGE.name.upper(), open_order_id=order_id, is_open=True) @@ -248,7 +249,7 @@ def app(config: dict) -> None: elif new_state == State.RUNNING: _process() # We need to sleep here because otherwise we would run into bittrex rate limit - time.sleep(25) + time.sleep(exchange.EXCHANGE.sleep_time) old_state = new_state except RuntimeError: telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index be8cf71b9..5115d2b0b 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -60,7 +60,7 @@ CONF_SCHEMA = { }, 'required': ['ask_last_balance'] }, - 'bittrex': {'$ref': '#/definitions/exchange'}, + 'exchange': {'$ref': '#/definitions/exchange'}, 'telegram': { 'type': 'object', 'properties': { @@ -76,6 +76,7 @@ CONF_SCHEMA = { 'exchange': { 'type': 'object', 'properties': { + 'name': {'type': 'string'}, 'enabled': {'type': 'boolean'}, 'key': {'type': 'string'}, 'secret': {'type': 'string'}, @@ -85,11 +86,11 @@ CONF_SCHEMA = { 'uniqueItems': True } }, - 'required': ['enabled', 'key', 'secret', 'pair_whitelist'] + 'required': ['name', 'enabled', 'key', 'secret', 'pair_whitelist'] } }, 'anyOf': [ - {'required': ['bittrex']} + {'required': ['exchange']} ], 'required': [ 'max_open_trades', diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 4f7129a9c..7f8bfbc69 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -41,7 +41,7 @@ class Trade(Base): __tablename__ = 'trades' id = Column(Integer, primary_key=True) - exchange = Column(Enum(exchange.Exchange), nullable=False) + exchange = Column(String, nullable=False) pair = Column(String, nullable=False) is_open = Column(Boolean, nullable=False, default=True) open_rate = Column(Float, nullable=False) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 80e330a51..5312125ed 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -257,7 +257,7 @@ def _forcesell(bot: Bot, update: Update) -> None: # Execute sell profit = trade.exec_sell_order(current_rate, balance) message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( - trade.exchange.name, + trade.exchange, trade.pair.replace('_', '/'), exchange.get_pair_detail_url(trade.pair), trade.close_rate, diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index dcb572157..fb3b7e511 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -44,10 +44,10 @@ def test_backtest(conf, pairs, mocker): trades = [] mocker.patch.dict('freqtrade.main._CONF', conf) for pair in pairs: - with open('tests/testdata/'+pair+'.json') as data_file: + 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('freqtrade.analyze.get_ticker_history', return_value=data) mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')) ticker = analyze_ticker(pair) # for each buy point diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 63da42e37..050d21ad4 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -5,8 +5,7 @@ from unittest.mock import MagicMock, call import pytest from jsonschema import validate -from freqtrade import exchange -from freqtrade.exchange import validate_pairs +from freqtrade.exchange import Exchanges from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \ get_target_bid from freqtrade.misc import CONF_SCHEMA @@ -28,7 +27,8 @@ def conf(): "bid_strategy": { "ask_last_balance": 0.0 }, - "bittrex": { + "exchange": { + "name": "bittrex", "enabled": True, "key": "key", "secret": "secret", @@ -61,22 +61,22 @@ def test_create_trade(conf, mocker): }), buy=MagicMock(return_value='mocked_order_id')) # Save state of current whitelist - whitelist = copy.deepcopy(conf['bittrex']['pair_whitelist']) + whitelist = copy.deepcopy(conf['exchange']['pair_whitelist']) init(conf, 'sqlite://') for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']: - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) Trade.session.add(trade) Trade.session.flush() assert trade is not None assert trade.open_rate == 0.072661 assert trade.pair == pair - assert trade.exchange == exchange.Exchange.BITTREX + assert trade.exchange == Exchanges.BITTREX.name assert trade.amount == 206.43811673387373 assert trade.stake_amount == 15.0 assert trade.is_open assert trade.open_date is not None - assert whitelist == conf['bittrex']['pair_whitelist'] + assert whitelist == conf['exchange']['pair_whitelist'] buy_signal.assert_has_calls( [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 376825c72..8cf280130 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchanges from freqtrade.persistence import Trade def test_exec_sell_order(mocker): @@ -9,7 +9,7 @@ def test_exec_sell_order(mocker): stake_amount=1.00, open_rate=0.50, amount=10.00, - exchange=Exchange.BITTREX, + exchange=Exchanges.BITTREX, open_order_id='mocked' ) profit = trade.exec_sell_order(1.00, 10.00) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index e01c5060a..fb9a618a0 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -6,7 +6,6 @@ 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 @@ -28,7 +27,8 @@ def conf(): "bid_strategy": { "ask_last_balance": 0.0 }, - "bittrex": { + "exchange": { + "name": "bittrex", "enabled": True, "key": "key", "secret": "secret", @@ -73,7 +73,7 @@ def test_status_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade Trade.session.add(trade) Trade.session.flush() @@ -98,7 +98,7 @@ def test_profit_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade trade.close_rate = 0.07256061 trade.close_profit = 100.00 @@ -128,7 +128,7 @@ def test_forcesell_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade Trade.session.add(trade) Trade.session.flush() @@ -156,7 +156,7 @@ def test_performance_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade trade.close_rate = 0.07256061 trade.close_profit = 100.00 From aef42336e626245c4bdc71255c0bb98c41dc52ac Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 6 Oct 2017 14:12:23 +0300 Subject: [PATCH 51/57] fixes to README.md - Fix the command for running unit tests - Add command to run backtest unit tests --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d4c457d8..3a2101d64 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,12 @@ There is also an [article](https://www.sales4k.com/blockchain/high-frequency-tra #### Execute tests ``` -$ python setup.py test +$ pytest +``` +This will by default skip the slow running backtest set. To run backtest set: + +``` +$ BACKTEST=true pytest ``` #### Docker From 95e5c2e6c1d48d7f84b9b6d8e415e844da339bb6 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 17:36:48 +0200 Subject: [PATCH 52/57] remove 'enabled' property in exchange config --- config.json.example | 1 - freqtrade/exchange.py | 3 --- freqtrade/misc.py | 3 +-- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/config.json.example b/config.json.example index 8f54a6a37..d2ae679db 100644 --- a/config.json.example +++ b/config.json.example @@ -15,7 +15,6 @@ }, "exchange": { "name": "bittrex", - "enabled": true, "key": "key", "secret": "secret", "pair_whitelist": [ diff --git a/freqtrade/exchange.py b/freqtrade/exchange.py index 0c1dd3ad9..35db8469d 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange.py @@ -48,9 +48,6 @@ def init(config: dict) -> None: if not exchange_class: raise RuntimeError('Exchange {} is not supported'.format(name)) - if not exchange_config.get('enabled', False): - raise RuntimeError('Exchange {} is disabled'.format(name)) - EXCHANGE = exchange_class(exchange_config) # Check if all pairs are available diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 5115d2b0b..585aee3de 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -77,7 +77,6 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'name': {'type': 'string'}, - 'enabled': {'type': 'boolean'}, 'key': {'type': 'string'}, 'secret': {'type': 'string'}, 'pair_whitelist': { @@ -86,7 +85,7 @@ CONF_SCHEMA = { 'uniqueItems': True } }, - 'required': ['name', 'enabled', 'key', 'secret', 'pair_whitelist'] + 'required': ['name', 'key', 'secret', 'pair_whitelist'] } }, 'anyOf': [ From ac3285003487d14542827152f491d35bda054d33 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 17:38:33 +0200 Subject: [PATCH 53/57] simplify exchange initialization --- freqtrade/exchange.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange.py b/freqtrade/exchange.py index 35db8469d..fa25b5bf5 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange.py @@ -37,15 +37,12 @@ def init(config: dict) -> None: logger.info('Instance is running with dry_run enabled') exchange_config = config['exchange'] - name = exchange_config['name'] # Find matching class for the given exchange name - exchange_class = None - for exchange in Exchanges: - if name.upper() == exchange.name: - exchange_class = exchange.value - break - if not exchange_class: + name = exchange_config['name'] + try: + exchange_class = Exchanges[name.upper()].value + except KeyError: raise RuntimeError('Exchange {} is not supported'.format(name)) EXCHANGE = exchange_class(exchange_config) From 34c774c067e51e3e38b5deb1985f9dc9d7f5e9d4 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 18:07:29 +0200 Subject: [PATCH 54/57] move exchange module content to exchange package and the interface to a new module --- freqtrade/{exchange.py => exchange/__init__.py} | 4 ++-- freqtrade/{exchanges => exchange}/bittrex.py | 2 +- freqtrade/{exchanges/__init__.py => exchange/interface.py} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename freqtrade/{exchange.py => exchange/__init__.py} (96%) rename freqtrade/{exchanges => exchange}/bittrex.py (98%) rename freqtrade/{exchanges/__init__.py => exchange/interface.py} (100%) diff --git a/freqtrade/exchange.py b/freqtrade/exchange/__init__.py similarity index 96% rename from freqtrade/exchange.py rename to freqtrade/exchange/__init__.py index fa25b5bf5..77a2d4b84 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange/__init__.py @@ -4,8 +4,8 @@ from typing import List import arrow -from freqtrade.exchanges import Exchange -from freqtrade.exchanges.bittrex import Bittrex +from freqtrade.exchange.bittrex import Bittrex +from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) diff --git a/freqtrade/exchanges/bittrex.py b/freqtrade/exchange/bittrex.py similarity index 98% rename from freqtrade/exchanges/bittrex.py rename to freqtrade/exchange/bittrex.py index 3b5cd4330..d2fd08ddc 100644 --- a/freqtrade/exchanges/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -5,7 +5,7 @@ import arrow import requests from bittrex.bittrex import Bittrex as _Bittrex -from freqtrade.exchanges import Exchange +from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) diff --git a/freqtrade/exchanges/__init__.py b/freqtrade/exchange/interface.py similarity index 100% rename from freqtrade/exchanges/__init__.py rename to freqtrade/exchange/interface.py From 2e368ef7aae01529b6e94924fcbfdb5ae3931fa8 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 18:10:45 +0200 Subject: [PATCH 55/57] docstring fix --- freqtrade/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 1d712007c..ce933fe44 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -156,7 +156,6 @@ def create_trade(stake_amount: float) -> Optional[Trade]: Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created :param stake_amount: amount of btc to spend - :param _exchange: """ logger.info('Creating new trade with stake_amount: %f ...', stake_amount) whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) From 75ea2c4e1a3bf4e0b8dad4691ae9c0cbf456c25d Mon Sep 17 00:00:00 2001 From: xsmile Date: Sun, 8 Oct 2017 23:01:36 +0200 Subject: [PATCH 56/57] add exchange package to manifest --- MANIFEST.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index b018ee274..ef776087e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include LICENSE include README.md include config.json.example +include freqtrade/exchange/*.py include freqtrade/rpc/*.py include freqtrade/tests/*.py -include freqtrade/tests/testdata/*.json \ No newline at end of file +include freqtrade/tests/testdata/*.json From bfac1936d944edb3ffeed9127b67685977b86e20 Mon Sep 17 00:00:00 2001 From: gcarq Date: Tue, 10 Oct 2017 17:54:42 +0200 Subject: [PATCH 57/57] version bump --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 98f461790..db6fd3728 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.10.0' +__version__ = '0.11.0' from . import main