From b9eb2662361919bd45728c3029c438fd62c8051a Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Fri, 6 Oct 2017 12:22:04 +0200 Subject: [PATCH 1/5] Exchange refactoring --- config.json.example | 11 +- freqtrade/analyze.py | 44 +++----- freqtrade/exchange.py | 154 +++++++++------------------- freqtrade/exchanges/__init__.py | 127 +++++++++++++++++++++++ freqtrade/exchanges/bittrex.py | 120 ++++++++++++++++++++++ freqtrade/main.py | 23 +++-- freqtrade/misc.py | 7 +- freqtrade/persistence.py | 2 +- freqtrade/rpc/telegram.py | 2 +- freqtrade/tests/test_backtesting.py | 4 +- freqtrade/tests/test_main.py | 14 +-- freqtrade/tests/test_persistence.py | 4 +- freqtrade/tests/test_telegram.py | 12 +-- 13 files changed, 350 insertions(+), 174 deletions(-) create mode 100644 freqtrade/exchanges/__init__.py create mode 100644 freqtrade/exchanges/bittrex.py diff --git a/config.json.example b/config.json.example index c2f6668d4..8f54a6a37 100644 --- a/config.json.example +++ b/config.json.example @@ -4,16 +4,17 @@ "stake_amount": 0.05, "dry_run": false, "minimal_roi": { - "60": 0.0, - "40": 0.01, - "20": 0.02, - "0": 0.03 + "60": 0.0, + "40": 0.01, + "20": 0.02, + "0": 0.03 }, "stoploss": -0.40, "bid_strategy": { "ask_last_balance": 0.0 }, - "bittrex": { + "exchange": { + "name": "bittrex", "enabled": true, "key": "key", "secret": "secret", diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index d8b86d945..159e1d137 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -1,36 +1,18 @@ +import logging import time from datetime import timedelta -import logging -import arrow -import requests -from pandas import DataFrame -import talib.abstract as ta +import arrow +import talib.abstract as ta +from pandas import DataFrame + +from freqtrade.exchange import get_ticker_history logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) -def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict: - """ - Request ticker data from Bittrex for a given currency pair - """ - url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks' - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', - } - params = { - 'marketName': pair.replace('_', '-'), - 'tickInterval': 'fiveMin', - '_': minimum_date.timestamp * 1000 - } - data = requests.get(url, params=params, headers=headers).json() - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return data - - def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame: """ Analyses the trend for the given pair @@ -43,6 +25,7 @@ def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame .sort_values('date') return df[df['date'].map(arrow.get) > minimum_date] + def populate_indicators(dataframe: DataFrame) -> DataFrame: """ Adds several different TA indicators to the given DataFrame @@ -87,17 +70,18 @@ def analyze_ticker(pair: str) -> DataFrame: :return DataFrame with ticker data and indicator data """ minimum_date = arrow.utcnow().shift(hours=-24) - data = get_ticker(pair, minimum_date) + data = get_ticker_history(pair, minimum_date) dataframe = parse_ticker_dataframe(data['result'], minimum_date) if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return dataframe - + dataframe = populate_indicators(dataframe) dataframe = populate_buy_trend(dataframe) return dataframe + def get_buy_signal(pair: str) -> bool: """ Calculates a buy signal based several technical analysis indicators @@ -144,9 +128,9 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None: ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy') ax1.legend() -# ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') + # ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX') ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI') -# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values)) + # ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values)) ax2.legend() # Fine-tune figure; make subplots close to each other and hide x ticks for @@ -160,7 +144,7 @@ if __name__ == '__main__': # Install PYQT5==5.9 manually if you want to test this helper function while True: test_pair = 'BTC_ETH' - #for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: - # get_buy_signal(pair) + # for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: + # get_buy_signal(pair) plot_dataframe(analyze_ticker(test_pair), test_pair) time.sleep(60) diff --git a/freqtrade/exchange.py b/freqtrade/exchange.py index 96bc13159..0c1dd3ad9 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange.py @@ -2,18 +2,23 @@ import enum import logging from typing import List -from bittrex.bittrex import Bittrex +import arrow + +from freqtrade.exchanges import Exchange +from freqtrade.exchanges.bittrex import Bittrex logger = logging.getLogger(__name__) # Current selected exchange -EXCHANGE = None -_API = None -_CONF = {} +EXCHANGE: Exchange = None +_CONF: dict = {} -class Exchange(enum.Enum): - BITTREX = 1 +class Exchanges(enum.Enum): + """ + Maps supported exchange names to correspondent classes. + """ + BITTREX = Bittrex def init(config: dict) -> None: @@ -24,22 +29,32 @@ def init(config: dict) -> None: :param config: config to use :return: None """ - global _API, EXCHANGE + global _CONF, EXCHANGE _CONF.update(config) if config['dry_run']: logger.info('Instance is running with dry_run enabled') - use_bittrex = config.get('bittrex', {}).get('enabled', False) - if use_bittrex: - EXCHANGE = Exchange.BITTREX - _API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret']) - else: - raise RuntimeError('No exchange specified. Aborting!') + exchange_config = config['exchange'] + name = exchange_config['name'] + + # Find matching class for the given exchange name + exchange_class = None + for exchange in Exchanges: + if name.upper() == exchange.name: + exchange_class = exchange.value + break + if not exchange_class: + raise RuntimeError('Exchange {} is not supported'.format(name)) + + if not exchange_config.get('enabled', False): + raise RuntimeError('Exchange {} is disabled'.format(name)) + + EXCHANGE = exchange_class(exchange_config) # Check if all pairs are available - validate_pairs(config[EXCHANGE.name.lower()]['pair_whitelist']) + validate_pairs(config['exchange']['pair_whitelist']) def validate_pairs(pairs: List[str]) -> None: @@ -49,131 +64,58 @@ def validate_pairs(pairs: List[str]) -> None: :param pairs: list of pairs :return: None """ - markets = get_markets() + markets = EXCHANGE.get_markets() for pair in pairs: if pair not in markets: raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower())) def buy(pair: str, rate: float, amount: float) -> str: - """ - Places a limit buy order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to purchase - :return: order_id of the placed buy order - """ if _CONF['dry_run']: return 'dry_run' - elif EXCHANGE == Exchange.BITTREX: - data = _API.buy_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return data['result']['uuid'] + + return EXCHANGE.buy(pair, rate, amount) def sell(pair: str, rate: float, amount: float) -> str: - """ - Places a limit sell order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to sell - :return: None - """ if _CONF['dry_run']: return 'dry_run' - elif EXCHANGE == Exchange.BITTREX: - data = _API.sell_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return data['result']['uuid'] + + return EXCHANGE.sell(pair, rate, amount) def get_balance(currency: str) -> float: - """ - Get account balance. - :param currency: currency as str, format: BTC - :return: float - """ if _CONF['dry_run']: return 999.9 - elif EXCHANGE == Exchange.BITTREX: - data = _API.get_balance(currency) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return float(data['result']['Balance'] or 0.0) + + return EXCHANGE.get_balance(currency) def get_ticker(pair: str) -> dict: - """ - Get Ticker for given pair. - :param pair: Pair as str, format: BTC_ETC - :return: dict - """ - if EXCHANGE == Exchange.BITTREX: - data = _API.get_ticker(pair.replace('_', '-')) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return { - 'bid': float(data['result']['Bid']), - 'ask': float(data['result']['Ask']), - 'last': float(data['result']['Last']), - } + return EXCHANGE.get_ticker(pair) + + +def get_ticker_history(pair: str, minimum_date: arrow.Arrow): + return EXCHANGE.get_ticker_history(pair, minimum_date) def cancel_order(order_id: str) -> None: - """ - Cancel order for given order_id - :param order_id: id as str - :return: None - """ if _CONF['dry_run']: - pass - elif EXCHANGE == Exchange.BITTREX: - data = _API.cancel(order_id) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) + return + + return EXCHANGE.cancel_order(order_id) def get_open_orders(pair: str) -> List[dict]: - """ - Get all open orders for given pair. - :param pair: Pair as str, format: BTC_ETC - :return: list of dicts - """ if _CONF['dry_run']: return [] - elif EXCHANGE == Exchange.BITTREX: - data = _API.get_open_orders(pair.replace('_', '-')) - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return [{ - 'id': entry['OrderUuid'], - 'type': entry['OrderType'], - 'opened': entry['Opened'], - 'rate': entry['PricePerUnit'], - 'amount': entry['Quantity'], - 'remaining': entry['QuantityRemaining'], - } for entry in data['result']] + + return EXCHANGE.get_open_orders(pair) def get_pair_detail_url(pair: str) -> str: - """ - Returns the market detail url for the given pair - :param pair: pair as str, format: BTC_ANT - :return: url as str - """ - if EXCHANGE == Exchange.BITTREX: - return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-')) + return EXCHANGE.get_pair_detail_url(pair) def get_markets() -> List[str]: - """ - Returns all available markets - :return: list of all available pairs - """ - if EXCHANGE == Exchange. BITTREX: - data = _API.get_markets() - if not data['success']: - raise RuntimeError('BITTREX: {}'.format(data['message'])) - return [m['MarketName'].replace('-', '_') for m in data['result']] + return EXCHANGE.get_markets() diff --git a/freqtrade/exchanges/__init__.py b/freqtrade/exchanges/__init__.py new file mode 100644 index 000000000..114ac9a6f --- /dev/null +++ b/freqtrade/exchanges/__init__.py @@ -0,0 +1,127 @@ +from abc import ABC, abstractmethod +from typing import List, Optional + +import arrow + + +class Exchange(ABC): + @property + def name(self) -> str: + """ + Name of the exchange. + :return: str representation of the class name + """ + return self.__class__.__name__ + + @property + @abstractmethod + def sleep_time(self) -> float: + """ + Sleep time in seconds for the main loop to avoid API rate limits. + :return: float + """ + + @abstractmethod + def buy(self, pair: str, rate: float, amount: float) -> str: + """ + Places a limit buy order. + :param pair: Pair as str, format: BTC_ETH + :param rate: Rate limit for order + :param amount: The amount to purchase + :return: order_id of the placed buy order + """ + + @abstractmethod + def sell(self, pair: str, rate: float, amount: float) -> str: + """ + Places a limit sell order. + :param pair: Pair as str, format: BTC_ETH + :param rate: Rate limit for order + :param amount: The amount to sell + :return: order_id of the placed sell order + """ + + @abstractmethod + def get_balance(self, currency: str) -> float: + """ + Gets account balance. + :param currency: Currency as str, format: BTC + :return: float + """ + + @abstractmethod + def get_ticker(self, pair: str) -> dict: + """ + Gets ticker for given pair. + :param pair: Pair as str, format: BTC_ETC + :return: dict, format: { + 'bid': float, + 'ask': float, + 'last': float + } + """ + + @abstractmethod + def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None) -> dict: + """ + Gets ticker history for given pair. + :param pair: Pair as str, format: BTC_ETC + :param minimum_date: Minimum date (optional) + :return: dict, format: { + 'success': bool, + 'message': str, + 'result': [ + { + 'O': float, (Open) + 'H': float, (High) + 'L': float, (Low) + 'C': float, (Close) + 'V': float, (Volume) + 'T': datetime, (Time) + 'BV': float, (Base Volume) + }, + ... + ] + } + """ + + @abstractmethod + def cancel_order(self, order_id: str) -> None: + """ + Cancels order for given order_id. + :param order_id: ID as str + :return: None + """ + + @abstractmethod + def get_open_orders(self, pair: str) -> List[dict]: + """ + Gets all open orders for given pair. + :param pair: Pair as str, format: BTC_ETC + :return: List of dicts, format: [ + { + 'id': str, + 'type': str, + 'opened': datetime, + 'rate': float, + 'amount': float, + 'remaining': int, + }, + ... + ] + """ + + @abstractmethod + def get_pair_detail_url(self, pair: str) -> str: + """ + Returns the market detail url for the given pair. + :param pair: Pair as str, format: BTC_ETC + :return: URL as str + """ + + @abstractmethod + def get_markets(self) -> List[str]: + """ + Returns all available markets. + :return: List of all available pairs + """ diff --git a/freqtrade/exchanges/bittrex.py b/freqtrade/exchanges/bittrex.py new file mode 100644 index 000000000..3b5cd4330 --- /dev/null +++ b/freqtrade/exchanges/bittrex.py @@ -0,0 +1,120 @@ +import logging +from typing import List, Optional + +import arrow +import requests +from bittrex.bittrex import Bittrex as _Bittrex + +from freqtrade.exchanges import Exchange + +logger = logging.getLogger(__name__) + +_API: _Bittrex = None +_EXCHANGE_CONF: dict = {} + + +class Bittrex(Exchange): + """ + Bittrex API wrapper. + """ + # Base URL and API endpoints + BASE_URL: str = 'https://www.bittrex.com' + TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks' + PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index' + # Ticker inveral + TICKER_INTERVAL: str = 'fiveMin' + # Sleep time to avoid rate limits, used in the main loop + SLEEP_TIME: float = 25 + + @property + def name(self) -> str: + return self.__class__.__name__ + + @property + def sleep_time(self) -> float: + return self.SLEEP_TIME + + def __init__(self, config: dict) -> None: + global _API, _EXCHANGE_CONF + + _EXCHANGE_CONF.update(config) + _API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret']) + + # Check if all pairs are available + markets = self.get_markets() + exchange_name = self.name + for pair in _EXCHANGE_CONF['pair_whitelist']: + if pair not in markets: + raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name)) + + def buy(self, pair: str, rate: float, amount: float) -> str: + data = _API.buy_limit(pair.replace('_', '-'), amount, rate) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return data['result']['uuid'] + + def sell(self, pair: str, rate: float, amount: float) -> str: + data = _API.sell_limit(pair.replace('_', '-'), amount, rate) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return data['result']['uuid'] + + def get_balance(self, currency: str) -> float: + data = _API.get_balance(currency) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return float(data['result']['Balance'] or 0.0) + + def get_ticker(self, pair: str) -> dict: + data = _API.get_ticker(pair.replace('_', '-')) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return { + 'bid': float(data['result']['Bid']), + 'ask': float(data['result']['Ask']), + 'last': float(data['result']['Last']), + } + + def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None): + url = self.TICKER_METHOD + headers = { + # TODO: Set as global setting + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36' + } + params = { + 'marketName': pair.replace('_', '-'), + 'tickInterval': self.TICKER_INTERVAL, + # TODO: Timestamp has no effect on API response + '_': minimum_date.timestamp * 1000 + } + data = requests.get(url, params=params, headers=headers).json() + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return data + + def cancel_order(self, order_id: str) -> None: + data = _API.cancel(order_id) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + + def get_open_orders(self, pair: str) -> List[dict]: + data = _API.get_open_orders(pair.replace('_', '-')) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return [{ + 'id': entry['OrderUuid'], + 'type': entry['OrderType'], + 'opened': entry['Opened'], + 'rate': entry['PricePerUnit'], + 'amount': entry['Quantity'], + 'remaining': entry['QuantityRemaining'], + } for entry in data['result']] + + def get_pair_detail_url(self, pair: str) -> str: + return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) + + def get_markets(self) -> List[str]: + data = _API.get_markets() + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + return [m['MarketName'].replace('-', '_') for m in data['result']] diff --git a/freqtrade/main.py b/freqtrade/main.py index 518c82a6d..1d712007c 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +import copy import json import logging import time @@ -6,12 +7,11 @@ import traceback from datetime import datetime from typing import Dict, Optional -import copy from jsonschema import validate -from freqtrade import exchange, persistence, __version__ +from freqtrade import __version__, exchange, persistence from freqtrade.analyze import get_buy_signal -from freqtrade.misc import State, update_state, get_state, CONF_SCHEMA +from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state from freqtrade.persistence import Trade from freqtrade.rpc import telegram @@ -34,7 +34,7 @@ def _process() -> None: if len(trades) < _CONF['max_open_trades']: try: # Create entity and execute trade - trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE) + trade = create_trade(float(_CONF['stake_amount'])) if trade: Trade.session.add(trade) else: @@ -91,7 +91,7 @@ def execute_sell(trade: Trade, current_rate: float) -> None: balance = exchange.get_balance(currency) profit = trade.exec_sell_order(current_rate, balance) message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( - trade.exchange.name, + trade.exchange, trade.pair.replace('_', '/'), exchange.get_pair_detail_url(trade.pair), trade.close_rate, @@ -142,6 +142,7 @@ def handle_trade(trade: Trade) -> None: except ValueError: logger.exception('Unable to handle open order') + def get_target_bid(ticker: Dict[str, float]) -> float: """ Calculates bid target between current ask price and last price """ if ticker['ask'] < ticker['last']: @@ -150,15 +151,15 @@ def get_target_bid(ticker: Dict[str, float]) -> float: return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) -def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]: +def create_trade(stake_amount: float) -> Optional[Trade]: """ Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created :param stake_amount: amount of btc to spend - :param _exchange: exchange to use + :param _exchange: """ logger.info('Creating new trade with stake_amount: %f ...', stake_amount) - whitelist = copy.deepcopy(_CONF[_exchange.name.lower()]['pair_whitelist']) + whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) # Check if stake_amount is fulfilled if exchange.get_balance(_CONF['stake_currency']) < stake_amount: raise ValueError( @@ -187,7 +188,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[ # Create trade entity and return message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format( - _exchange.name, + exchange.EXCHANGE.name.upper(), pair.replace('_', '/'), exchange.get_pair_detail_url(pair), open_rate @@ -199,7 +200,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[ open_rate=open_rate, open_date=datetime.utcnow(), amount=amount, - exchange=_exchange, + exchange=exchange.EXCHANGE.name.upper(), open_order_id=order_id, is_open=True) @@ -248,7 +249,7 @@ def app(config: dict) -> None: elif new_state == State.RUNNING: _process() # We need to sleep here because otherwise we would run into bittrex rate limit - time.sleep(25) + time.sleep(exchange.EXCHANGE.sleep_time) old_state = new_state except RuntimeError: telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index be8cf71b9..5115d2b0b 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -60,7 +60,7 @@ CONF_SCHEMA = { }, 'required': ['ask_last_balance'] }, - 'bittrex': {'$ref': '#/definitions/exchange'}, + 'exchange': {'$ref': '#/definitions/exchange'}, 'telegram': { 'type': 'object', 'properties': { @@ -76,6 +76,7 @@ CONF_SCHEMA = { 'exchange': { 'type': 'object', 'properties': { + 'name': {'type': 'string'}, 'enabled': {'type': 'boolean'}, 'key': {'type': 'string'}, 'secret': {'type': 'string'}, @@ -85,11 +86,11 @@ CONF_SCHEMA = { 'uniqueItems': True } }, - 'required': ['enabled', 'key', 'secret', 'pair_whitelist'] + 'required': ['name', 'enabled', 'key', 'secret', 'pair_whitelist'] } }, 'anyOf': [ - {'required': ['bittrex']} + {'required': ['exchange']} ], 'required': [ 'max_open_trades', diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 4f7129a9c..7f8bfbc69 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -41,7 +41,7 @@ class Trade(Base): __tablename__ = 'trades' id = Column(Integer, primary_key=True) - exchange = Column(Enum(exchange.Exchange), nullable=False) + exchange = Column(String, nullable=False) pair = Column(String, nullable=False) is_open = Column(Boolean, nullable=False, default=True) open_rate = Column(Float, nullable=False) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 80e330a51..5312125ed 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -257,7 +257,7 @@ def _forcesell(bot: Bot, update: Update) -> None: # Execute sell profit = trade.exec_sell_order(current_rate, balance) message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( - trade.exchange.name, + trade.exchange, trade.pair.replace('_', '/'), exchange.get_pair_detail_url(trade.pair), trade.close_rate, diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index dcb572157..fb3b7e511 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -44,10 +44,10 @@ def test_backtest(conf, pairs, mocker): trades = [] mocker.patch.dict('freqtrade.main._CONF', conf) for pair in pairs: - with open('tests/testdata/'+pair+'.json') as data_file: + with open('freqtrade/tests/testdata/'+pair+'.json') as data_file: data = json.load(data_file) - mocker.patch('freqtrade.analyze.get_ticker', return_value=data) + mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data) mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')) ticker = analyze_ticker(pair) # for each buy point diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 63da42e37..050d21ad4 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -5,8 +5,7 @@ from unittest.mock import MagicMock, call import pytest from jsonschema import validate -from freqtrade import exchange -from freqtrade.exchange import validate_pairs +from freqtrade.exchange import Exchanges from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \ get_target_bid from freqtrade.misc import CONF_SCHEMA @@ -28,7 +27,8 @@ def conf(): "bid_strategy": { "ask_last_balance": 0.0 }, - "bittrex": { + "exchange": { + "name": "bittrex", "enabled": True, "key": "key", "secret": "secret", @@ -61,22 +61,22 @@ def test_create_trade(conf, mocker): }), buy=MagicMock(return_value='mocked_order_id')) # Save state of current whitelist - whitelist = copy.deepcopy(conf['bittrex']['pair_whitelist']) + whitelist = copy.deepcopy(conf['exchange']['pair_whitelist']) init(conf, 'sqlite://') for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']: - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) Trade.session.add(trade) Trade.session.flush() assert trade is not None assert trade.open_rate == 0.072661 assert trade.pair == pair - assert trade.exchange == exchange.Exchange.BITTREX + assert trade.exchange == Exchanges.BITTREX.name assert trade.amount == 206.43811673387373 assert trade.stake_amount == 15.0 assert trade.is_open assert trade.open_date is not None - assert whitelist == conf['bittrex']['pair_whitelist'] + assert whitelist == conf['exchange']['pair_whitelist'] buy_signal.assert_has_calls( [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 376825c72..8cf280130 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchanges from freqtrade.persistence import Trade def test_exec_sell_order(mocker): @@ -9,7 +9,7 @@ def test_exec_sell_order(mocker): stake_amount=1.00, open_rate=0.50, amount=10.00, - exchange=Exchange.BITTREX, + exchange=Exchanges.BITTREX, open_order_id='mocked' ) profit = trade.exec_sell_order(1.00, 10.00) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index e01c5060a..fb9a618a0 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -6,7 +6,6 @@ import pytest from jsonschema import validate from telegram import Bot, Update, Message, Chat -from freqtrade import exchange from freqtrade.main import init, create_trade from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA from freqtrade.persistence import Trade @@ -28,7 +27,8 @@ def conf(): "bid_strategy": { "ask_last_balance": 0.0 }, - "bittrex": { + "exchange": { + "name": "bittrex", "enabled": True, "key": "key", "secret": "secret", @@ -73,7 +73,7 @@ def test_status_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade Trade.session.add(trade) Trade.session.flush() @@ -98,7 +98,7 @@ def test_profit_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade trade.close_rate = 0.07256061 trade.close_profit = 100.00 @@ -128,7 +128,7 @@ def test_forcesell_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade Trade.session.add(trade) Trade.session.flush() @@ -156,7 +156,7 @@ def test_performance_handle(conf, update, mocker): init(conf, 'sqlite://') # Create some test data - trade = create_trade(15.0, exchange.Exchange.BITTREX) + trade = create_trade(15.0) assert trade trade.close_rate = 0.07256061 trade.close_profit = 100.00 From 95e5c2e6c1d48d7f84b9b6d8e415e844da339bb6 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 17:36:48 +0200 Subject: [PATCH 2/5] remove 'enabled' property in exchange config --- config.json.example | 1 - freqtrade/exchange.py | 3 --- freqtrade/misc.py | 3 +-- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/config.json.example b/config.json.example index 8f54a6a37..d2ae679db 100644 --- a/config.json.example +++ b/config.json.example @@ -15,7 +15,6 @@ }, "exchange": { "name": "bittrex", - "enabled": true, "key": "key", "secret": "secret", "pair_whitelist": [ diff --git a/freqtrade/exchange.py b/freqtrade/exchange.py index 0c1dd3ad9..35db8469d 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange.py @@ -48,9 +48,6 @@ def init(config: dict) -> None: if not exchange_class: raise RuntimeError('Exchange {} is not supported'.format(name)) - if not exchange_config.get('enabled', False): - raise RuntimeError('Exchange {} is disabled'.format(name)) - EXCHANGE = exchange_class(exchange_config) # Check if all pairs are available diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 5115d2b0b..585aee3de 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -77,7 +77,6 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'name': {'type': 'string'}, - 'enabled': {'type': 'boolean'}, 'key': {'type': 'string'}, 'secret': {'type': 'string'}, 'pair_whitelist': { @@ -86,7 +85,7 @@ CONF_SCHEMA = { 'uniqueItems': True } }, - 'required': ['name', 'enabled', 'key', 'secret', 'pair_whitelist'] + 'required': ['name', 'key', 'secret', 'pair_whitelist'] } }, 'anyOf': [ From ac3285003487d14542827152f491d35bda054d33 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 17:38:33 +0200 Subject: [PATCH 3/5] simplify exchange initialization --- freqtrade/exchange.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange.py b/freqtrade/exchange.py index 35db8469d..fa25b5bf5 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange.py @@ -37,15 +37,12 @@ def init(config: dict) -> None: logger.info('Instance is running with dry_run enabled') exchange_config = config['exchange'] - name = exchange_config['name'] # Find matching class for the given exchange name - exchange_class = None - for exchange in Exchanges: - if name.upper() == exchange.name: - exchange_class = exchange.value - break - if not exchange_class: + name = exchange_config['name'] + try: + exchange_class = Exchanges[name.upper()].value + except KeyError: raise RuntimeError('Exchange {} is not supported'.format(name)) EXCHANGE = exchange_class(exchange_config) From 34c774c067e51e3e38b5deb1985f9dc9d7f5e9d4 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 18:07:29 +0200 Subject: [PATCH 4/5] move exchange module content to exchange package and the interface to a new module --- freqtrade/{exchange.py => exchange/__init__.py} | 4 ++-- freqtrade/{exchanges => exchange}/bittrex.py | 2 +- freqtrade/{exchanges/__init__.py => exchange/interface.py} | 0 3 files changed, 3 insertions(+), 3 deletions(-) rename freqtrade/{exchange.py => exchange/__init__.py} (96%) rename freqtrade/{exchanges => exchange}/bittrex.py (98%) rename freqtrade/{exchanges/__init__.py => exchange/interface.py} (100%) diff --git a/freqtrade/exchange.py b/freqtrade/exchange/__init__.py similarity index 96% rename from freqtrade/exchange.py rename to freqtrade/exchange/__init__.py index fa25b5bf5..77a2d4b84 100644 --- a/freqtrade/exchange.py +++ b/freqtrade/exchange/__init__.py @@ -4,8 +4,8 @@ from typing import List import arrow -from freqtrade.exchanges import Exchange -from freqtrade.exchanges.bittrex import Bittrex +from freqtrade.exchange.bittrex import Bittrex +from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) diff --git a/freqtrade/exchanges/bittrex.py b/freqtrade/exchange/bittrex.py similarity index 98% rename from freqtrade/exchanges/bittrex.py rename to freqtrade/exchange/bittrex.py index 3b5cd4330..d2fd08ddc 100644 --- a/freqtrade/exchanges/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -5,7 +5,7 @@ import arrow import requests from bittrex.bittrex import Bittrex as _Bittrex -from freqtrade.exchanges import Exchange +from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) diff --git a/freqtrade/exchanges/__init__.py b/freqtrade/exchange/interface.py similarity index 100% rename from freqtrade/exchanges/__init__.py rename to freqtrade/exchange/interface.py From 2e368ef7aae01529b6e94924fcbfdb5ae3931fa8 Mon Sep 17 00:00:00 2001 From: xsmile <> Date: Sat, 7 Oct 2017 18:10:45 +0200 Subject: [PATCH 5/5] docstring fix --- freqtrade/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 1d712007c..ce933fe44 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -156,7 +156,6 @@ def create_trade(stake_amount: float) -> Optional[Trade]: Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created :param stake_amount: amount of btc to spend - :param _exchange: """ logger.info('Creating new trade with stake_amount: %f ...', stake_amount) whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])