From c53f2e6b88703cf71d27d7bb56e4bc34b017625c Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 13 Oct 2017 19:56:31 +0200 Subject: [PATCH 01/12] pin to python-bittrex==0.2.0 --- freqtrade/exchange/bittrex.py | 8 ++++++-- requirements.txt | 2 +- setup.py | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index cb85aaf87..8305b2a3b 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -3,7 +3,7 @@ from typing import List, Optional import arrow import requests -from bittrex.bittrex import Bittrex as _Bittrex +from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0 from freqtrade.exchange.interface import Exchange @@ -34,7 +34,11 @@ 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, + ) def buy(self, pair: str, rate: float, amount: float) -> str: data = _API.buy_limit(pair.replace('_', '-'), amount, rate) diff --git a/requirements.txt b/requirements.txt index be3a292f0..0cd5b1f2b 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.0 arrow==0.10.0 diff --git a/setup.py b/setup.py index e89fd2eee..f8c515861 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.0', '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, From b3ed0151f05f6d03a3c9b36b4056dea48e9f8c32 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 13 Oct 2017 19:58:46 +0200 Subject: [PATCH 02/12] update get_ticker_history --- freqtrade/exchange/bittrex.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 8305b2a3b..ebd87cc18 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -2,7 +2,6 @@ import logging from typing import List, Optional import arrow -import requests from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0 from freqtrade.exchange.interface import Exchange @@ -19,7 +18,6 @@ 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' @@ -69,18 +67,7 @@ class Bittrex(Exchange): } 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 From 93b729a5d559ea9bf2b1d93840347516431fe3da Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 13 Oct 2017 21:09:55 +0200 Subject: [PATCH 03/12] replace get_ticker with get_orderbook --- freqtrade/exchange/__init__.py | 6 +++--- freqtrade/exchange/bittrex.py | 11 +++++------ freqtrade/exchange/interface.py | 24 ++++++++++++++++++------ freqtrade/main.py | 15 ++++++++------- freqtrade/rpc/telegram.py | 6 +++--- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 77a2d4b84..17e9649af 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,8 +85,8 @@ def get_balance(currency: str) -> float: return EXCHANGE.get_balance(currency) -def get_ticker(pair: str) -> dict: - 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): diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index ebd87cc18..6519f7ff5 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -1,5 +1,5 @@ import logging -from typing import List, Optional +from typing import List, Optional, Dict import arrow from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0 @@ -56,14 +56,13 @@ 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_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): diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py index 114ac9a6f..67c910504 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,26 @@ class Exchange(ABC): """ @abstractmethod - def get_ticker(self, pair: str) -> dict: + def get_orderbook(self, pair: str, top_most: Optional[int] = None) -> Dict[str, List[Dict]]: """ - Gets ticker for given pair. + 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': float, - 'ask': float, - 'last': float + 'bid': [ + { + 'Quantity': float, + 'Rate': float, + }, + ... + ], + 'ask': [ + { + 'Quantity': float, + 'Rate': float, + }, + ... + ] } """ diff --git a/freqtrade/main.py b/freqtrade/main.py index ce933fe44..2e8163b62 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -5,7 +5,7 @@ import logging import time import traceback from datetime import datetime -from typing import Dict, Optional +from typing import Dict, Optional, List from jsonschema import validate @@ -134,7 +134,7 @@ def handle_trade(trade: Trade) -> None: logger.debug('Handling open trade %s ...', trade) - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] if should_sell(trade, current_rate, datetime.utcnow()): execute_sell(trade, current_rate) return @@ -143,12 +143,13 @@ def handle_trade(trade: Trade) -> None: logger.exception('Unable to handle open order') -def get_target_bid(ticker: Dict[str, float]) -> float: +def get_target_bid(ticker: Dict[str, List[Dict]]) -> float: """ Calculates bid target between current ask price and last price """ - if ticker['ask'] < ticker['last']: - return ticker['ask'] + # TODO: refactor this + ask = ticker['ask'][0]['Rate'] + bid = ticker['bid'][0]['Rate'] balance = _CONF['bid_strategy']['ask_last_balance'] - return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) + return ask + balance * (bid - ask) def create_trade(stake_amount: float) -> Optional[Trade]: @@ -181,7 +182,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]: else: return None - open_rate = get_target_bid(exchange.get_ticker(pair)) + open_rate = get_target_bid(exchange.get_orderbook(pair, top_most=1)) amount = stake_amount / open_rate order_id = exchange.buy(pair, open_rate, amount) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5312125ed..a09e6087e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -99,7 +99,7 @@ def _status(bot: Bot, update: Update) -> None: else: for trade in trades: # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) orders = exchange.get_open_orders(trade.pair) orders = [o for o in orders if o['id'] == trade.open_order_id] @@ -156,7 +156,7 @@ def _profit(bot: Bot, update: Update) -> None: profit = trade.close_profit else: # Get current rate - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) profit_amounts.append((profit / 100) * trade.stake_amount) @@ -250,7 +250,7 @@ def _forcesell(bot: Bot, update: Update) -> None: send_msg('There is no open trade with ID: `{}`'.format(trade_id)) return # Get current rate - current_rate = exchange.get_ticker(trade.pair)['bid'] + current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] # Get available balance currency = trade.pair.split('_')[1] balance = exchange.get_balance(currency) From 20b432c73ddb43e84bf1045ad3d4c2d411a0ee7e Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 13 Oct 2017 21:47:49 +0200 Subject: [PATCH 04/12] update buy --- freqtrade/exchange/bittrex.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 6519f7ff5..979ed9492 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -2,7 +2,8 @@ import logging from typing import List, Optional, Dict import arrow -from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0 +from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, TICKINTERVAL_FIVEMIN, ORDERTYPE_LIMIT, \ + TIMEINEFFECT_GOOD_TIL_CANCELLED from freqtrade.exchange.interface import Exchange @@ -20,7 +21,7 @@ class Bittrex(Exchange): BASE_URL: str = 'https://www.bittrex.com' 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 @@ -39,10 +40,16 @@ class Bittrex(Exchange): ) 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) From ff393e54b871c492b9032a90803aa1e049a80622 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 13 Oct 2017 21:48:43 +0200 Subject: [PATCH 05/12] update sell --- freqtrade/exchange/bittrex.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 979ed9492..c686cd8c1 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -52,10 +52,16 @@ class Bittrex(Exchange): 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) From 57c835449edc3e79e6be4965f6cc5f9bedddfabb Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 13 Oct 2017 23:10:51 +0200 Subject: [PATCH 06/12] refactor get_target_bid to use bid as low and ask as high value --- freqtrade/main.py | 19 +++++--- freqtrade/misc.py | 4 +- freqtrade/tests/test_main.py | 80 +++++++++++++++++++++++++------- freqtrade/tests/test_telegram.py | 60 +++++++++++++++++------- 4 files changed, 120 insertions(+), 43 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 2e8163b62..e4e0f8335 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -143,13 +143,18 @@ def handle_trade(trade: Trade) -> None: logger.exception('Unable to handle open order') -def get_target_bid(ticker: Dict[str, List[Dict]]) -> float: - """ Calculates bid target between current ask price and last price """ - # TODO: refactor this - ask = ticker['ask'][0]['Rate'] - bid = ticker['bid'][0]['Rate'] - balance = _CONF['bid_strategy']['ask_last_balance'] - return ask + balance * (bid - ask) +def get_target_bid(orderbook: Dict[str, List[Dict]]) -> float: + """ + Calculates bid target between + bid and ask prices from the given orderbook + :param orderbook: + :return: target bit as float + """ + default_target = 1.0 # Use ask price as default + ask = orderbook['ask'][0]['Rate'] # Get lowest ask + bid = orderbook['bid'][0]['Rate'] # Get highest bid + balance = _CONF['bid_strategy'].get('bid_ask_balance', default_target) + 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/tests/test_main.py b/freqtrade/tests/test_main.py index 050d21ad4..3479ba898 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,16 +48,22 @@ 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) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 + get_orderbook=MagicMock(return_value={ + 'bid': [{ + 'Quantity': 1, + 'Rate': 0.07256061 + }], + 'ask': [{ + 'Quantity': 1, + 'Rate': 0.072661 + }] }), buy=MagicMock(return_value='mocked_order_id')) # Save state of current whitelist @@ -82,15 +88,21 @@ 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()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_ticker=MagicMock(return_value={ - 'bid': 0.17256061, - 'ask': 0.172661, - 'last': 0.17256061 + get_orderbook=MagicMock(return_value={ + 'bid': [{ + 'Quantity': 1, + 'Rate': 0.17256061 + }], + 'ask': [{ + 'Quantity': 1, + 'Rate': 0.172661 + }] }), buy=MagicMock(return_value='mocked_order_id')) trade = Trade.query.filter(Trade.is_open.is_(True)).first() @@ -101,6 +113,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 +126,47 @@ 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}}) + orderbook = { + 'bid': [{ + 'Quantity': 10, + 'Rate': 10 + }], + 'ask': [{ + 'Quantity': 20, + 'Rate': 20 + }] + } + assert get_target_bid(orderbook) == 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}}) + orderbook = { + 'bid': [{ + 'Quantity': 10, + 'Rate': 10 + }], + 'ask': [{ + 'Quantity': 20, + 'Rate': 20 + }] + } + assert get_target_bid(orderbook) == 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}}) + orderbook = { + 'bid': [{ + 'Quantity': 10, + 'Rate': 10 + }], + 'ask': [{ + 'Quantity': 20, + 'Rate': 20 + }] + } + assert get_target_bid(orderbook) == 15 diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index fb9a618a0..d3d54197d 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) @@ -64,10 +65,15 @@ def test_status_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 + get_orderbook=MagicMock(return_value={ + 'bid': [{ + 'Quantity': 1, + 'Rate': 0.07256061 + }], + 'ask': [{ + 'Quantity': 1, + 'Rate': 0.072661 + }] }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -82,6 +88,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) @@ -89,10 +96,15 @@ def test_profit_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 + get_orderbook=MagicMock(return_value={ + 'bid': [{ + 'Quantity': 1, + 'Rate': 0.07256061 + }], + 'ask': [{ + 'Quantity': 1, + 'Rate': 0.072661 + }] }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -112,6 +124,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) @@ -119,10 +132,15 @@ def test_forcesell_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 + get_orderbook=MagicMock(return_value={ + 'bid': [{ + 'Quantity': 1, + 'Rate': 0.07256061 + }], + 'ask': [{ + 'Quantity': 1, + 'Rate': 0.072661 + }] }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -140,6 +158,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) @@ -147,10 +166,15 @@ def test_performance_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_ticker=MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061 + get_orderbook=MagicMock(return_value={ + 'bid': [{ + 'Quantity': 1, + 'Rate': 0.07256061 + }], + 'ask': [{ + 'Quantity': 1, + 'Rate': 0.072661 + }] }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -171,6 +195,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 +209,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() From 4493eeb8c37106d9d8a3491c3b0ea92bde0da21c Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 15 Oct 2017 18:33:01 +0200 Subject: [PATCH 07/12] rename ask_last_balance to bid_ask_balance and default to 1.0 --- config.json.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From e482372f4a33ee4a466889706e845f833f7b6ace Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 15 Oct 2017 18:34:16 +0200 Subject: [PATCH 08/12] fail if bid_ask_balance is not set --- freqtrade/main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index e4e0f8335..9fe817db4 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -150,10 +150,9 @@ def get_target_bid(orderbook: Dict[str, List[Dict]]) -> float: :param orderbook: :return: target bit as float """ - default_target = 1.0 # Use ask price as default ask = orderbook['ask'][0]['Rate'] # Get lowest ask bid = orderbook['bid'][0]['Rate'] # Get highest bid - balance = _CONF['bid_strategy'].get('bid_ask_balance', default_target) + balance = _CONF['bid_strategy']['bid_ask_balance'] return bid + balance * (ask - bid) From 42e67da9e701ef404d2aab206c1d1290c64ea468 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 15 Oct 2017 18:59:52 +0200 Subject: [PATCH 09/12] implement get_ticker as wrapper around get_orderbook --- freqtrade/exchange/__init__.py | 4 +++ freqtrade/exchange/bittrex.py | 7 ++++ freqtrade/exchange/interface.py | 11 ++++++ freqtrade/main.py | 11 +++--- freqtrade/rpc/telegram.py | 6 ++-- freqtrade/tests/test_main.py | 60 +++++--------------------------- freqtrade/tests/test_telegram.py | 48 +++++++------------------ 7 files changed, 51 insertions(+), 96 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 17e9649af..fbf8f38b2 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -85,6 +85,10 @@ def get_balance(currency: str) -> float: return EXCHANGE.get_balance(currency) +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) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index c686cd8c1..d45028588 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -69,6 +69,13 @@ 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[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']: diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py index 67c910504..56629c5b4 100644 --- a/freqtrade/exchange/interface.py +++ b/freqtrade/exchange/interface.py @@ -49,6 +49,17 @@ class Exchange(ABC): :return: float """ + @abstractmethod + 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 + } + """ + @abstractmethod def get_orderbook(self, pair: str, top_most: Optional[int] = None) -> Dict[str, List[Dict]]: """ diff --git a/freqtrade/main.py b/freqtrade/main.py index 9fe817db4..7cb88cffe 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -134,7 +134,7 @@ def handle_trade(trade: Trade) -> None: logger.debug('Handling open trade %s ...', trade) - current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] + current_rate = exchange.get_ticker(trade.pair)['bid'] if should_sell(trade, current_rate, datetime.utcnow()): execute_sell(trade, current_rate) return @@ -143,15 +143,14 @@ def handle_trade(trade: Trade) -> None: logger.exception('Unable to handle open order') -def get_target_bid(orderbook: Dict[str, List[Dict]]) -> float: +def get_target_bid(ticker: Dict[str, float]) -> float: """ Calculates bid target between bid and ask prices from the given orderbook - :param orderbook: + :param ticker: ticker data :return: target bit as float """ - ask = orderbook['ask'][0]['Rate'] # Get lowest ask - bid = orderbook['bid'][0]['Rate'] # Get highest bid + ask, bid = ticker['ask'], ticker['bid'] balance = _CONF['bid_strategy']['bid_ask_balance'] return bid + balance * (ask - bid) @@ -186,7 +185,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]: else: return None - open_rate = get_target_bid(exchange.get_orderbook(pair, top_most=1)) + open_rate = get_target_bid(exchange.get_ticker(pair)) amount = stake_amount / open_rate order_id = exchange.buy(pair, open_rate, amount) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a09e6087e..5312125ed 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -99,7 +99,7 @@ def _status(bot: Bot, update: Update) -> None: else: for trade in trades: # calculate profit and send message to user - current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] + current_rate = exchange.get_ticker(trade.pair)['bid'] current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) orders = exchange.get_open_orders(trade.pair) orders = [o for o in orders if o['id'] == trade.open_order_id] @@ -156,7 +156,7 @@ def _profit(bot: Bot, update: Update) -> None: profit = trade.close_profit else: # Get current rate - current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] + current_rate = exchange.get_ticker(trade.pair)['bid'] profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) profit_amounts.append((profit / 100) * trade.stake_amount) @@ -250,7 +250,7 @@ def _forcesell(bot: Bot, update: Update) -> None: send_msg('There is no open trade with ID: `{}`'.format(trade_id)) return # Get current rate - current_rate = exchange.get_orderbook(trade.pair, top_most=1)['bid'][0]['Rate'] + current_rate = exchange.get_ticker(trade.pair)['bid'] # Get available balance currency = trade.pair.split('_')[1] balance = exchange.get_balance(currency) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 3479ba898..a0907b8a8 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -55,15 +55,9 @@ def test_create_trade(conf, mocker): mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_orderbook=MagicMock(return_value={ - 'bid': [{ - 'Quantity': 1, - 'Rate': 0.07256061 - }], - 'ask': [{ - 'Quantity': 1, - 'Rate': 0.072661 - }] + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, }), buy=MagicMock(return_value='mocked_order_id')) # Save state of current whitelist @@ -94,15 +88,9 @@ def test_handle_trade(conf, mocker): mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_orderbook=MagicMock(return_value={ - 'bid': [{ - 'Quantity': 1, - 'Rate': 0.17256061 - }], - 'ask': [{ - 'Quantity': 1, - 'Rate': 0.172661 - }] + get_ticker=MagicMock(return_value={ + 'bid': 0.17256061, + 'ask': 0.172661, }), buy=MagicMock(return_value='mocked_order_id')) trade = Trade.query.filter(Trade.is_open.is_(True)).first() @@ -129,44 +117,14 @@ def test_close_trade(conf, mocker): def test_balance_fully_bid_side(mocker): mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 0.0}}) - orderbook = { - 'bid': [{ - 'Quantity': 10, - 'Rate': 10 - }], - 'ask': [{ - 'Quantity': 20, - 'Rate': 20 - }] - } - assert get_target_bid(orderbook) == 10 + assert get_target_bid({'bid': 10, 'ask': 20}) == 10 def test_balance_fully_ask_side(mocker): mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 1.0}}) - orderbook = { - 'bid': [{ - 'Quantity': 10, - 'Rate': 10 - }], - 'ask': [{ - 'Quantity': 20, - 'Rate': 20 - }] - } - assert get_target_bid(orderbook) == 20 + assert get_target_bid({'bid': 10, 'ask': 20}) == 20 def test_balance_half(mocker): mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 0.5}}) - orderbook = { - 'bid': [{ - 'Quantity': 10, - 'Rate': 10 - }], - 'ask': [{ - 'Quantity': 20, - 'Rate': 20 - }] - } - assert get_target_bid(orderbook) == 15 + assert get_target_bid({'bid': 10, 'ask': 20}) == 15 diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index d3d54197d..2d1f4f201 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -65,15 +65,9 @@ def test_status_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_orderbook=MagicMock(return_value={ - 'bid': [{ - 'Quantity': 1, - 'Rate': 0.07256061 - }], - 'ask': [{ - 'Quantity': 1, - 'Rate': 0.072661 - }] + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -96,15 +90,9 @@ def test_profit_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_orderbook=MagicMock(return_value={ - 'bid': [{ - 'Quantity': 1, - 'Rate': 0.07256061 - }], - 'ask': [{ - 'Quantity': 1, - 'Rate': 0.072661 - }] + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -132,15 +120,9 @@ def test_forcesell_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_orderbook=MagicMock(return_value={ - 'bid': [{ - 'Quantity': 1, - 'Rate': 0.07256061 - }], - 'ask': [{ - 'Quantity': 1, - 'Rate': 0.072661 - }] + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') @@ -166,15 +148,9 @@ def test_performance_handle(conf, update, mocker): mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), - get_orderbook=MagicMock(return_value={ - 'bid': [{ - 'Quantity': 1, - 'Rate': 0.07256061 - }], - 'ask': [{ - 'Quantity': 1, - 'Rate': 0.072661 - }] + get_ticker=MagicMock(return_value={ + 'bid': 0.07256061, + 'ask': 0.072661, }), buy=MagicMock(return_value='mocked_order_id')) init(conf, 'sqlite://') From 52ca364fa4e964ddf9aea870099580f79554d010 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 15 Oct 2017 19:02:33 +0200 Subject: [PATCH 10/12] remove unused imports --- freqtrade/main.py | 2 +- freqtrade/persistence.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 7cb88cffe..41af59e08 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -5,7 +5,7 @@ import logging import time import traceback from datetime import datetime -from typing import Dict, Optional, List +from typing import Dict, Optional from jsonschema import validate diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 7f8bfbc69..59dddd1a8 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 From 28501b7be984d13224a21d8632f45268d2de5cfa Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 15 Oct 2017 19:15:24 +0200 Subject: [PATCH 11/12] adapt README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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. From 47fabf632cb97f985c35423a7e55cb687b72665a Mon Sep 17 00:00:00 2001 From: gcarq Date: Sun, 15 Oct 2017 19:27:19 +0200 Subject: [PATCH 12/12] set calls_per_second --- freqtrade/exchange/bittrex.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index d45028588..0e9fbf208 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -37,6 +37,7 @@ class Bittrex(Exchange): 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: