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/bin/freqtrade b/bin/freqtrade new file mode 100644 index 000000000..3ecd3a520 --- /dev/null +++ b/bin/freqtrade @@ -0,0 +1,4 @@ +#!/usr/bin/env python + +from freqtrade.main import main +main() \ No newline at end of file diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py new file mode 100644 index 000000000..98f461790 --- /dev/null +++ b/freqtrade/__init__.py @@ -0,0 +1,3 @@ +__version__ = '0.10.0' + +from . import main diff --git a/analyze.py b/freqtrade/analyze.py similarity index 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 94% rename from main.py rename to freqtrade/main.py index 8f2256bb4..6c80d490c 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 = {} @@ -97,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('_', '/'), @@ -238,7 +233,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None: def app(config: dict) -> None: """ - Main function which handles the application state + Main loop which handles the application state :param config: config as dict :return: None """ @@ -269,8 +264,17 @@ def app(config: dict) -> None: telegram.send_msg('*Status:* `Trader has stopped`') -if __name__ == '__main__': +def main(): + """ + Loads and validates the config and starts the main loop + :return: None + """ + global _CONF with open('config.json') as file: _CONF = json.load(file) validate(_CONF, CONF_SCHEMA) app(_CONF) + + +if __name__ == '__main__': + main() diff --git a/misc.py b/freqtrade/misc.py similarity index 100% rename from misc.py rename to freqtrade/misc.py diff --git a/persistence.py b/freqtrade/persistence.py similarity index 98% rename from persistence.py rename to freqtrade/persistence.py index dd917a750..4f7129a9c 100644 --- a/persistence.py +++ b/freqtrade/persistence.py @@ -5,11 +5,9 @@ from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker - from sqlalchemy.types import Enum -import exchange - +from freqtrade import exchange _CONF = {} diff --git a/rpc/__init__.py b/freqtrade/rpc/__init__.py similarity index 100% rename from rpc/__init__.py rename to freqtrade/rpc/__init__.py diff --git a/rpc/telegram.py b/freqtrade/rpc/telegram.py similarity index 98% rename from rpc/telegram.py rename to freqtrade/rpc/telegram.py index 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 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 new file mode 100644 index 000000000..2d8d40990 --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +from setuptools import setup + +from freqtrade import __version__ + + +setup(name='freqtrade', + version=__version__, + description='Simple High Frequency Trading Bot for crypto currencies', + url='https://github.com/gcarq/freqtrade', + author='gcarq and contributors', + author_email='michael.egger@tsn.at', + license='GPLv3', + packages=['freqtrade'], + scripts=['bin/freqtrade'], + setup_requires=['pytest-runner'], + tests_require=['pytest'], + install_requires=[ + 'python-bittrex==0.1.3', + 'SQLAlchemy==1.1.13', + 'python-telegram-bot==8.0', + 'arrow==0.10.0', + 'requests==2.18.4', + 'urllib3==1.22', + 'wrapt==1.10.11', + 'pandas==0.20.3', + 'scikit-learn==0.19.0', + 'scipy==0.19.1', + 'jsonschema==2.6.0', + 'TA-Lib==0.4.10', + ], + dependency_links=[ + "git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex-0.1.3" + ], + include_package_data=True, + zip_safe=False, + classifiers=[ + 'Programming Language :: Python :: 3.6', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Topic :: Office/Business :: Financial :: Investment', + 'Intended Audience :: Science/Research', + ])