diff --git a/README.md b/README.md index 4388f00e3..655dd806b 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,8 @@ profit dips below -10% for a given trade. This parameter is optional. Possible values are `running` or `stopped`. (default=`running`) If the value is `stopped` the bot has to be started with `/start` first. -`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will -use the `last` price and values between those interpolate between ask and last +`bid_ask_balance` sets the bidding price. Value `0.0` will use `bid` price, `1.0` will +use the `ask` price and values between those interpolate between bid and ask price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary. diff --git a/config.json.example b/config.json.example index 685189087..926027625 100644 --- a/config.json.example +++ b/config.json.example @@ -11,7 +11,7 @@ }, "stoploss": -0.40, "bid_strategy": { - "ask_last_balance": 0.0 + "bid_ask_balance": 1.0 }, "exchange": { "name": "bittrex", diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 77a2d4b84..fbf8f38b2 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,6 +1,6 @@ import enum import logging -from typing import List +from typing import List, Optional, Dict import arrow @@ -85,10 +85,14 @@ def get_balance(currency: str) -> float: return EXCHANGE.get_balance(currency) -def get_ticker(pair: str) -> dict: +def get_ticker(pair: str) -> Dict[str, float]: return EXCHANGE.get_ticker(pair) +def get_orderbook(pair: str, top_most: Optional[int] = None) -> Dict[str, List[Dict]]: + return EXCHANGE.get_orderbook(pair, top_most) + + def get_ticker_history(pair: str, minimum_date: arrow.Arrow): return EXCHANGE.get_ticker_history(pair, minimum_date) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index cb85aaf87..0e9fbf208 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -1,9 +1,9 @@ import logging -from typing import List, Optional +from typing import List, Optional, Dict import arrow -import requests -from bittrex.bittrex import Bittrex as _Bittrex +from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, TICKINTERVAL_FIVEMIN, ORDERTYPE_LIMIT, \ + TIMEINEFFECT_GOOD_TIL_CANCELLED from freqtrade.exchange.interface import Exchange @@ -19,10 +19,9 @@ class Bittrex(Exchange): """ # 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' + TICKER_INTERVAL: str = TICKINTERVAL_FIVEMIN # Sleep time to avoid rate limits, used in the main loop SLEEP_TIME: float = 25 @@ -34,19 +33,36 @@ class Bittrex(Exchange): global _API, _EXCHANGE_CONF _EXCHANGE_CONF.update(config) - _API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret']) + _API = _Bittrex( + api_key=_EXCHANGE_CONF['key'], + api_secret=_EXCHANGE_CONF['secret'], + api_version=API_V2_0, + calls_per_second=10, + ) def buy(self, pair: str, rate: float, amount: float) -> str: - data = _API.buy_limit(pair.replace('_', '-'), amount, rate) + data = _API.trade_buy( + market=pair.replace('_', '-'), + order_type=ORDERTYPE_LIMIT, + quantity=amount, + rate=rate, + time_in_effect=TIMEINEFFECT_GOOD_TIL_CANCELLED, + ) if not data['success']: raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) - return data['result']['uuid'] + return data['result']['OrderId'] def sell(self, pair: str, rate: float, amount: float) -> str: - data = _API.sell_limit(pair.replace('_', '-'), amount, rate) + data = _API.trade_sell( + market=pair.replace('_', '-'), + order_type=ORDERTYPE_LIMIT, + quantity=amount, + rate=rate, + time_in_effect=TIMEINEFFECT_GOOD_TIL_CANCELLED, + ) if not data['success']: raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) - return data['result']['uuid'] + return data['result']['OrderId'] def get_balance(self, currency: str) -> float: data = _API.get_balance(currency) @@ -54,29 +70,24 @@ class Bittrex(Exchange): 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('_', '-')) + def get_ticker(self, pair: str) -> Dict[str, float]: + data = self.get_orderbook(pair, top_most=1) + return { + 'bid': data['bid'][0]['Rate'], + 'ask': data['ask'][0]['Rate'], + } + + def get_orderbook(self, pair: str, top_most: Optional[int] = None) -> Dict[str, List[Dict]]: + data = _API.get_orderbook(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']), + 'bid': data['result']['buy'][:top_most], + 'ask': data['result']['sell'][:top_most], } 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() + data = _API.get_candles(pair.replace('_', '-'), self.TICKER_INTERVAL) if not data['success']: raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) return data diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py index 114ac9a6f..56629c5b4 100644 --- a/freqtrade/exchange/interface.py +++ b/freqtrade/exchange/interface.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import List, Optional +from typing import List, Optional, Dict import arrow @@ -50,14 +50,37 @@ class Exchange(ABC): """ @abstractmethod - def get_ticker(self, pair: str) -> dict: + def get_ticker(self, pair: str) -> Dict[str, float]: """ Gets ticker for given pair. :param pair: Pair as str, format: BTC_ETC :return: dict, format: { 'bid': float, - 'ask': float, - 'last': float + 'ask': float + } + """ + + @abstractmethod + def get_orderbook(self, pair: str, top_most: Optional[int] = None) -> Dict[str, List[Dict]]: + """ + Gets orderbook for given pair. + :param pair: Pair as str, format: BTC_ETC + :param top_most: only return n top_most bids/sells (optional) + :return: dict, format: { + 'bid': [ + { + 'Quantity': float, + 'Rate': float, + }, + ... + ], + 'ask': [ + { + 'Quantity': float, + 'Rate': float, + }, + ... + ] } """ diff --git a/freqtrade/main.py b/freqtrade/main.py index 68277adaa..1001545f4 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -145,11 +145,15 @@ def handle_trade(trade: Trade) -> None: def get_target_bid(ticker: Dict[str, float]) -> float: - """ Calculates bid target between current ask price and last price """ - if ticker['ask'] < ticker['last']: - return ticker['ask'] - balance = _CONF['bid_strategy']['ask_last_balance'] - return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) + """ + Calculates bid target between + bid and ask prices from the given orderbook + :param ticker: ticker data + :return: target bit as float + """ + ask, bid = ticker['ask'], ticker['bid'] + balance = _CONF['bid_strategy']['bid_ask_balance'] + return bid + balance * (ask - bid) def create_trade(stake_amount: float) -> Optional[Trade]: diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 585aee3de..74251a344 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -51,14 +51,14 @@ CONF_SCHEMA = { 'bid_strategy': { 'type': 'object', 'properties': { - 'ask_last_balance': { + 'bid_ask_balance': { 'type': 'number', 'minimum': 0, 'maximum': 1, 'exclusiveMaximum': False }, }, - 'required': ['ask_last_balance'] + 'required': ['bid_ask_balance'] }, 'exchange': {'$ref': '#/definitions/exchange'}, 'telegram': { diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index fa51b1349..dc9078d5f 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -5,7 +5,6 @@ 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 from freqtrade import exchange diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 050d21ad4..a0907b8a8 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -25,7 +25,7 @@ def conf(): "0": 0.02 }, "bid_strategy": { - "ask_last_balance": 0.0 + "bid_ask_balance": 1.0 }, "exchange": { "name": "bittrex", @@ -48,6 +48,7 @@ def conf(): validate(configuration, CONF_SCHEMA) return configuration + def test_create_trade(conf, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -57,7 +58,6 @@ def test_create_trade(conf, mocker): 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 @@ -82,6 +82,7 @@ def test_create_trade(conf, mocker): [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] ) + def test_handle_trade(conf, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) @@ -90,7 +91,6 @@ def test_handle_trade(conf, mocker): 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() @@ -101,6 +101,7 @@ def test_handle_trade(conf, mocker): assert trade.close_date is not None assert trade.open_order_id == 'dry_run' + def test_close_trade(conf, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) trade = Trade.query.filter(Trade.is_open.is_(True)).first() @@ -113,14 +114,17 @@ def test_close_trade(conf, mocker): assert closed assert not trade.is_open + +def test_balance_fully_bid_side(mocker): + mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 0.0}}) + assert get_target_bid({'bid': 10, 'ask': 20}) == 10 + + 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 + mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 1.0}}) + assert get_target_bid({'bid': 10, 'ask': 20}) == 20 -def test_balance_fully_last_side(mocker): - mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}) - assert get_target_bid({'ask': 20, 'last': 10}) == 10 -def test_balance_when_last_bigger_than_ask(mocker): - mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}) - assert get_target_bid({'ask': 5, 'last': 10}) == 5 +def test_balance_half(mocker): + mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 0.5}}) + assert get_target_bid({'bid': 10, 'ask': 20}) == 15 diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index fb9a618a0..2d1f4f201 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -25,7 +25,7 @@ def conf(): "0": 0.02 }, "bid_strategy": { - "ask_last_balance": 0.0 + "bid_ask_balance": 1.0, }, "exchange": { "name": "bittrex", @@ -46,6 +46,7 @@ def conf(): validate(configuration, CONF_SCHEMA) return configuration + @pytest.fixture def update(): _update = Update(0) @@ -67,7 +68,6 @@ def test_status_handle(conf, update, mocker): get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, - 'last': 0.07256061 }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -82,6 +82,7 @@ def test_status_handle(conf, update, mocker): assert msg_mock.call_count == 2 assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] + def test_profit_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -92,7 +93,6 @@ def test_profit_handle(conf, update, mocker): get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, - 'last': 0.07256061 }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -112,6 +112,7 @@ def test_profit_handle(conf, update, mocker): assert msg_mock.call_count == 2 assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] + def test_forcesell_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -122,7 +123,6 @@ def test_forcesell_handle(conf, update, mocker): get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, - 'last': 0.07256061 }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -140,6 +140,7 @@ def test_forcesell_handle(conf, update, mocker): assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0] assert '0.072561' in msg_mock.call_args_list[-1][0][0] + def test_performance_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) @@ -150,7 +151,6 @@ def test_performance_handle(conf, update, mocker): get_ticker=MagicMock(return_value={ 'bid': 0.07256061, 'ask': 0.072661, - 'last': 0.07256061 }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -171,6 +171,7 @@ def test_performance_handle(conf, update, mocker): assert 'Performance' in msg_mock.call_args_list[-1][0][0] assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0] + def test_start_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) msg_mock = MagicMock() @@ -184,6 +185,7 @@ def test_start_handle(conf, update, mocker): assert get_state() == State.RUNNING assert msg_mock.call_count == 0 + def test_stop_handle(conf, update, mocker): mocker.patch.dict('freqtrade.main._CONF', conf) msg_mock = MagicMock() diff --git a/requirements.txt b/requirements.txt index 69379b025..9a55cecff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ --e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex +-e git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex SQLAlchemy==1.1.14 python-telegram-bot==8.1.1 arrow==0.10.0 diff --git a/setup.py b/setup.py index eced2b5b3..4db29865d 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup(name='freqtrade', setup_requires=['pytest-runner'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ - 'python-bittrex==0.1.3', + 'python-bittrex==0.2.0', 'SQLAlchemy==1.1.13', 'python-telegram-bot==8.1.1', 'arrow==0.10.0', @@ -29,7 +29,7 @@ setup(name='freqtrade', 'TA-Lib==0.4.10', ], dependency_links=[ - "git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex-0.1.3" + "git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex-0.2.0" ], include_package_data=True, zip_safe=False,