This commit is contained in:
Michael Egger 2017-10-29 19:23:54 +00:00 committed by GitHub
commit eec1311ff9
12 changed files with 110 additions and 63 deletions

View File

@ -45,8 +45,8 @@ profit dips below -10% for a given trade. This parameter is optional.
Possible values are `running` or `stopped`. (default=`running`) Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first. 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 `bid_ask_balance` sets the bidding price. Value `0.0` will use `bid` price, `1.0` will
use the `last` price and values between those interpolate between ask and last 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 price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary. end up paying more then would probably have been necessary.

View File

@ -11,7 +11,7 @@
}, },
"stoploss": -0.40, "stoploss": -0.40,
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "bid_ask_balance": 1.0
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",

View File

@ -1,6 +1,6 @@
import enum import enum
import logging import logging
from typing import List from typing import List, Optional, Dict
import arrow import arrow
@ -85,10 +85,14 @@ def get_balance(currency: str) -> float:
return EXCHANGE.get_balance(currency) return EXCHANGE.get_balance(currency)
def get_ticker(pair: str) -> dict: def get_ticker(pair: str) -> Dict[str, float]:
return EXCHANGE.get_ticker(pair) 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): def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
return EXCHANGE.get_ticker_history(pair, minimum_date) return EXCHANGE.get_ticker_history(pair, minimum_date)

View File

@ -1,9 +1,9 @@
import logging import logging
from typing import List, Optional from typing import List, Optional, Dict
import arrow import arrow
import requests from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, TICKINTERVAL_FIVEMIN, ORDERTYPE_LIMIT, \
from bittrex.bittrex import Bittrex as _Bittrex TIMEINEFFECT_GOOD_TIL_CANCELLED
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
@ -19,10 +19,9 @@ class Bittrex(Exchange):
""" """
# Base URL and API endpoints # Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com' 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' PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
# Ticker inveral # Ticker inveral
TICKER_INTERVAL: str = 'fiveMin' TICKER_INTERVAL: str = TICKINTERVAL_FIVEMIN
# Sleep time to avoid rate limits, used in the main loop # Sleep time to avoid rate limits, used in the main loop
SLEEP_TIME: float = 25 SLEEP_TIME: float = 25
@ -34,19 +33,36 @@ class Bittrex(Exchange):
global _API, _EXCHANGE_CONF global _API, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config) _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: 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']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) 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: 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']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data['result']['uuid'] return data['result']['OrderId']
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency) data = _API.get_balance(currency)
@ -54,29 +70,24 @@ class Bittrex(Exchange):
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return float(data['result']['Balance'] or 0.0) return float(data['result']['Balance'] or 0.0)
def get_ticker(self, pair: str) -> dict: def get_ticker(self, pair: str) -> Dict[str, float]:
data = _API.get_ticker(pair.replace('_', '-')) 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']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return { return {
'bid': float(data['result']['Bid']), 'bid': data['result']['buy'][:top_most],
'ask': float(data['result']['Ask']), 'ask': data['result']['sell'][:top_most],
'last': float(data['result']['Last']),
} }
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None): def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None):
url = self.TICKER_METHOD data = _API.get_candles(pair.replace('_', '-'), self.TICKER_INTERVAL)
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']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data return data

View File

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List, Optional from typing import List, Optional, Dict
import arrow import arrow
@ -50,14 +50,37 @@ class Exchange(ABC):
""" """
@abstractmethod @abstractmethod
def get_ticker(self, pair: str) -> dict: def get_ticker(self, pair: str) -> Dict[str, float]:
""" """
Gets ticker for given pair. Gets ticker for given pair.
:param pair: Pair as str, format: BTC_ETC :param pair: Pair as str, format: BTC_ETC
:return: dict, format: { :return: dict, format: {
'bid': float, 'bid': float,
'ask': float, 'ask': float
'last': 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,
},
...
]
} }
""" """

View File

@ -145,11 +145,15 @@ def handle_trade(trade: Trade) -> None:
def get_target_bid(ticker: Dict[str, float]) -> float: def get_target_bid(ticker: Dict[str, float]) -> float:
""" Calculates bid target between current ask price and last price """ """
if ticker['ask'] < ticker['last']: Calculates bid target between
return ticker['ask'] bid and ask prices from the given orderbook
balance = _CONF['bid_strategy']['ask_last_balance'] :param ticker: ticker data
return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) :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]: def create_trade(stake_amount: float) -> Optional[Trade]:

View File

@ -51,14 +51,14 @@ CONF_SCHEMA = {
'bid_strategy': { 'bid_strategy': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'ask_last_balance': { 'bid_ask_balance': {
'type': 'number', 'type': 'number',
'minimum': 0, 'minimum': 0,
'maximum': 1, 'maximum': 1,
'exclusiveMaximum': False 'exclusiveMaximum': False
}, },
}, },
'required': ['ask_last_balance'] 'required': ['bid_ask_balance']
}, },
'exchange': {'$ref': '#/definitions/exchange'}, 'exchange': {'$ref': '#/definitions/exchange'},
'telegram': { 'telegram': {

View File

@ -5,7 +5,6 @@ from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.types import Enum
from freqtrade import exchange from freqtrade import exchange

View File

@ -25,7 +25,7 @@ def conf():
"0": 0.02 "0": 0.02
}, },
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "bid_ask_balance": 1.0
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@ -48,6 +48,7 @@ def conf():
validate(configuration, CONF_SCHEMA) validate(configuration, CONF_SCHEMA)
return configuration return configuration
def test_create_trade(conf, mocker): def test_create_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) 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={ get_ticker=MagicMock(return_value={
'bid': 0.07256061, 'bid': 0.07256061,
'ask': 0.072661, 'ask': 0.072661,
'last': 0.07256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
# Save state of current whitelist # 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')] [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
) )
def test_handle_trade(conf, mocker): def test_handle_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) 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={ get_ticker=MagicMock(return_value={
'bid': 0.17256061, 'bid': 0.17256061,
'ask': 0.172661, 'ask': 0.172661,
'last': 0.17256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
trade = Trade.query.filter(Trade.is_open.is_(True)).first() 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.close_date is not None
assert trade.open_order_id == 'dry_run' assert trade.open_order_id == 'dry_run'
def test_close_trade(conf, mocker): def test_close_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
trade = Trade.query.filter(Trade.is_open.is_(True)).first() trade = Trade.query.filter(Trade.is_open.is_(True)).first()
@ -113,14 +114,17 @@ def test_close_trade(conf, mocker):
assert closed assert closed
assert not trade.is_open 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): def test_balance_fully_ask_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}) mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 1.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 20 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): def test_balance_half(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}) mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'bid_ask_balance': 0.5}})
assert get_target_bid({'ask': 5, 'last': 10}) == 5 assert get_target_bid({'bid': 10, 'ask': 20}) == 15

View File

@ -25,7 +25,7 @@ def conf():
"0": 0.02 "0": 0.02
}, },
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "bid_ask_balance": 1.0,
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@ -46,6 +46,7 @@ def conf():
validate(configuration, CONF_SCHEMA) validate(configuration, CONF_SCHEMA)
return configuration return configuration
@pytest.fixture @pytest.fixture
def update(): def update():
_update = Update(0) _update = Update(0)
@ -67,7 +68,6 @@ def test_status_handle(conf, update, mocker):
get_ticker=MagicMock(return_value={ get_ticker=MagicMock(return_value={
'bid': 0.07256061, 'bid': 0.07256061,
'ask': 0.072661, 'ask': 0.072661,
'last': 0.07256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://') init(conf, 'sqlite://')
@ -82,6 +82,7 @@ def test_status_handle(conf, update, mocker):
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
def test_profit_handle(conf, update, mocker): def test_profit_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) 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={ get_ticker=MagicMock(return_value={
'bid': 0.07256061, 'bid': 0.07256061,
'ask': 0.072661, 'ask': 0.072661,
'last': 0.07256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://') init(conf, 'sqlite://')
@ -112,6 +112,7 @@ def test_profit_handle(conf, update, mocker):
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
def test_forcesell_handle(conf, update, mocker): def test_forcesell_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) 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={ get_ticker=MagicMock(return_value={
'bid': 0.07256061, 'bid': 0.07256061,
'ask': 0.072661, 'ask': 0.072661,
'last': 0.07256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://') 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 '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 '0.072561' in msg_mock.call_args_list[-1][0][0]
def test_performance_handle(conf, update, mocker): def test_performance_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) 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={ get_ticker=MagicMock(return_value={
'bid': 0.07256061, 'bid': 0.07256061,
'ask': 0.072661, 'ask': 0.072661,
'last': 0.07256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://') 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 'Performance' in msg_mock.call_args_list[-1][0][0]
assert 'BTC_ETH 100.00%' 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): def test_start_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock() msg_mock = MagicMock()
@ -184,6 +185,7 @@ def test_start_handle(conf, update, mocker):
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
assert msg_mock.call_count == 0 assert msg_mock.call_count == 0
def test_stop_handle(conf, update, mocker): def test_stop_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock() msg_mock = MagicMock()

View File

@ -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 SQLAlchemy==1.1.14
python-telegram-bot==8.1.1 python-telegram-bot==8.1.1
arrow==0.10.0 arrow==0.10.0

View File

@ -15,7 +15,7 @@ setup(name='freqtrade',
setup_requires=['pytest-runner'], setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[ install_requires=[
'python-bittrex==0.1.3', 'python-bittrex==0.2.0',
'SQLAlchemy==1.1.13', 'SQLAlchemy==1.1.13',
'python-telegram-bot==8.1.1', 'python-telegram-bot==8.1.1',
'arrow==0.10.0', 'arrow==0.10.0',
@ -29,7 +29,7 @@ setup(name='freqtrade',
'TA-Lib==0.4.10', 'TA-Lib==0.4.10',
], ],
dependency_links=[ 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, include_package_data=True,
zip_safe=False, zip_safe=False,