Merge pull request #58 from xsmile/exchange-interface

Exchange refactoring
This commit is contained in:
Michael Egger 2017-10-08 15:56:50 +02:00 committed by GitHub
commit 5e0f143a38
14 changed files with 415 additions and 248 deletions

View File

@ -13,8 +13,8 @@
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },
"bittrex": { "exchange": {
"enabled": true, "name": "bittrex",
"key": "key", "key": "key",
"secret": "secret", "secret": "secret",
"pair_whitelist": [ "pair_whitelist": [

View File

@ -1,36 +1,18 @@
import logging
import time import time
from datetime import timedelta 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, logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) 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: def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
""" """
Analyses the trend for the given pair 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') .sort_values('date')
return df[df['date'].map(arrow.get) > minimum_date] return df[df['date'].map(arrow.get) > minimum_date]
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(dataframe: DataFrame) -> DataFrame:
""" """
Adds several different TA indicators to the given DataFrame Adds several different TA indicators to the given DataFrame
@ -87,7 +70,7 @@ def analyze_ticker(pair: str) -> DataFrame:
:return DataFrame with ticker data and indicator data :return DataFrame with ticker data and indicator data
""" """
minimum_date = arrow.utcnow().shift(hours=-24) 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) dataframe = parse_ticker_dataframe(data['result'], minimum_date)
if dataframe.empty: if dataframe.empty:
@ -98,6 +81,7 @@ def analyze_ticker(pair: str) -> DataFrame:
dataframe = populate_buy_trend(dataframe) dataframe = populate_buy_trend(dataframe)
return dataframe return dataframe
def get_buy_signal(pair: str) -> bool: def get_buy_signal(pair: str) -> bool:
""" """
Calculates a buy signal based several technical analysis indicators 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.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
ax1.legend() 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, 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() ax2.legend()
# Fine-tune figure; make subplots close to each other and hide x ticks for # 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 # Install PYQT5==5.9 manually if you want to test this helper function
while True: while True:
test_pair = 'BTC_ETH' test_pair = 'BTC_ETH'
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: # for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair) # get_buy_signal(pair)
plot_dataframe(analyze_ticker(test_pair), test_pair) plot_dataframe(analyze_ticker(test_pair), test_pair)
time.sleep(60) time.sleep(60)

View File

@ -1,179 +0,0 @@
import enum
import logging
from typing import List
from bittrex.bittrex import Bittrex
logger = logging.getLogger(__name__)
# Current selected exchange
EXCHANGE = None
_API = None
_CONF = {}
class Exchange(enum.Enum):
BITTREX = 1
def init(config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _API, 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!')
# Check if all pairs are available
validate_pairs(config[EXCHANGE.name.lower()]['pair_whitelist'])
def validate_pairs(pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises RuntimeError if one pair is not available.
:param pairs: list of pairs
:return: None
"""
markets = 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']
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']
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)
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']),
}
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']))
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']]
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('_', '-'))
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']]

View File

@ -0,0 +1,115 @@
import enum
import logging
from typing import List
import arrow
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
# Current selected exchange
EXCHANGE: Exchange = None
_CONF: dict = {}
class Exchanges(enum.Enum):
"""
Maps supported exchange names to correspondent classes.
"""
BITTREX = Bittrex
def init(config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _CONF, EXCHANGE
_CONF.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
# Find matching class for the given exchange name
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)
# Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist'])
def validate_pairs(pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises RuntimeError if one pair is not available.
:param pairs: list of pairs
:return: None
"""
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:
if _CONF['dry_run']:
return 'dry_run'
return EXCHANGE.buy(pair, rate, amount)
def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
return 'dry_run'
return EXCHANGE.sell(pair, rate, amount)
def get_balance(currency: str) -> float:
if _CONF['dry_run']:
return 999.9
return EXCHANGE.get_balance(currency)
def get_ticker(pair: str) -> dict:
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:
if _CONF['dry_run']:
return
return EXCHANGE.cancel_order(order_id)
def get_open_orders(pair: str) -> List[dict]:
if _CONF['dry_run']:
return []
return EXCHANGE.get_open_orders(pair)
def get_pair_detail_url(pair: str) -> str:
return EXCHANGE.get_pair_detail_url(pair)
def get_markets() -> List[str]:
return EXCHANGE.get_markets()

View File

@ -0,0 +1,120 @@
import logging
from typing import List, Optional
import arrow
import requests
from bittrex.bittrex import Bittrex as _Bittrex
from freqtrade.exchange.interface 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']]

View File

@ -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
"""

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
import copy
import json import json
import logging import logging
import time import time
@ -6,12 +7,11 @@ import traceback
from datetime import datetime from datetime import datetime
from typing import Dict, Optional from typing import Dict, Optional
import copy
from jsonschema import validate 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.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.persistence import Trade
from freqtrade.rpc import telegram from freqtrade.rpc import telegram
@ -34,7 +34,7 @@ def _process() -> None:
if len(trades) < _CONF['max_open_trades']: if len(trades) < _CONF['max_open_trades']:
try: try:
# Create entity and execute trade # Create entity and execute trade
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE) trade = create_trade(float(_CONF['stake_amount']))
if trade: if trade:
Trade.session.add(trade) Trade.session.add(trade)
else: else:
@ -91,7 +91,7 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
balance = exchange.get_balance(currency) balance = exchange.get_balance(currency)
profit = trade.exec_sell_order(current_rate, balance) profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name, trade.exchange,
trade.pair.replace('_', '/'), trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair), exchange.get_pair_detail_url(trade.pair),
trade.close_rate, trade.close_rate,
@ -142,6 +142,7 @@ def handle_trade(trade: Trade) -> None:
except ValueError: except ValueError:
logger.exception('Unable to handle open order') logger.exception('Unable to handle open order')
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 """ """ Calculates bid target between current ask price and last price """
if ticker['ask'] < ticker['last']: if ticker['ask'] < ticker['last']:
@ -150,15 +151,14 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) 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, Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend :param stake_amount: amount of btc to spend
:param _exchange: exchange to use
""" """
logger.info('Creating new trade with stake_amount: %f ...', stake_amount) 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 # Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount: if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError( raise ValueError(
@ -187,7 +187,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
# Create trade entity and return # Create trade entity and return
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format( message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
_exchange.name, exchange.EXCHANGE.name.upper(),
pair.replace('_', '/'), pair.replace('_', '/'),
exchange.get_pair_detail_url(pair), exchange.get_pair_detail_url(pair),
open_rate open_rate
@ -199,7 +199,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
open_rate=open_rate, open_rate=open_rate,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
amount=amount, amount=amount,
exchange=_exchange, exchange=exchange.EXCHANGE.name.upper(),
open_order_id=order_id, open_order_id=order_id,
is_open=True) is_open=True)
@ -248,7 +248,7 @@ def app(config: dict) -> None:
elif new_state == State.RUNNING: elif new_state == State.RUNNING:
_process() _process()
# We need to sleep here because otherwise we would run into bittrex rate limit # 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 old_state = new_state
except RuntimeError: except RuntimeError:
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())) telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))

View File

@ -60,7 +60,7 @@ CONF_SCHEMA = {
}, },
'required': ['ask_last_balance'] 'required': ['ask_last_balance']
}, },
'bittrex': {'$ref': '#/definitions/exchange'}, 'exchange': {'$ref': '#/definitions/exchange'},
'telegram': { 'telegram': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@ -76,7 +76,7 @@ CONF_SCHEMA = {
'exchange': { 'exchange': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'enabled': {'type': 'boolean'}, 'name': {'type': 'string'},
'key': {'type': 'string'}, 'key': {'type': 'string'},
'secret': {'type': 'string'}, 'secret': {'type': 'string'},
'pair_whitelist': { 'pair_whitelist': {
@ -85,11 +85,11 @@ CONF_SCHEMA = {
'uniqueItems': True 'uniqueItems': True
} }
}, },
'required': ['enabled', 'key', 'secret', 'pair_whitelist'] 'required': ['name', 'key', 'secret', 'pair_whitelist']
} }
}, },
'anyOf': [ 'anyOf': [
{'required': ['bittrex']} {'required': ['exchange']}
], ],
'required': [ 'required': [
'max_open_trades', 'max_open_trades',

View File

@ -41,7 +41,7 @@ class Trade(Base):
__tablename__ = 'trades' __tablename__ = 'trades'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
exchange = Column(Enum(exchange.Exchange), nullable=False) exchange = Column(String, nullable=False)
pair = Column(String, nullable=False) pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True) is_open = Column(Boolean, nullable=False, default=True)
open_rate = Column(Float, nullable=False) open_rate = Column(Float, nullable=False)

View File

@ -257,7 +257,7 @@ def _forcesell(bot: Bot, update: Update) -> None:
# Execute sell # Execute sell
profit = trade.exec_sell_order(current_rate, balance) profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name, trade.exchange,
trade.pair.replace('_', '/'), trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair), exchange.get_pair_detail_url(trade.pair),
trade.close_rate, trade.close_rate,

View File

@ -47,7 +47,7 @@ def test_backtest(conf, pairs, mocker):
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file: with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
data = json.load(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')) mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
ticker = analyze_ticker(pair) ticker = analyze_ticker(pair)
# for each buy point # for each buy point

View File

@ -5,8 +5,7 @@ from unittest.mock import MagicMock, call
import pytest import pytest
from jsonschema import validate from jsonschema import validate
from freqtrade import exchange from freqtrade.exchange import Exchanges
from freqtrade.exchange import validate_pairs
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \ from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
get_target_bid get_target_bid
from freqtrade.misc import CONF_SCHEMA from freqtrade.misc import CONF_SCHEMA
@ -28,7 +27,8 @@ def conf():
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },
"bittrex": { "exchange": {
"name": "bittrex",
"enabled": True, "enabled": True,
"key": "key", "key": "key",
"secret": "secret", "secret": "secret",
@ -61,22 +61,22 @@ def test_create_trade(conf, mocker):
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
# Save state of current whitelist # Save state of current whitelist
whitelist = copy.deepcopy(conf['bittrex']['pair_whitelist']) whitelist = copy.deepcopy(conf['exchange']['pair_whitelist'])
init(conf, 'sqlite://') init(conf, 'sqlite://')
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']: 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.add(trade)
Trade.session.flush() Trade.session.flush()
assert trade is not None assert trade is not None
assert trade.open_rate == 0.072661 assert trade.open_rate == 0.072661
assert trade.pair == pair assert trade.pair == pair
assert trade.exchange == exchange.Exchange.BITTREX assert trade.exchange == Exchanges.BITTREX.name
assert trade.amount == 206.43811673387373 assert trade.amount == 206.43811673387373
assert trade.stake_amount == 15.0 assert trade.stake_amount == 15.0
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert whitelist == conf['bittrex']['pair_whitelist'] assert whitelist == conf['exchange']['pair_whitelist']
buy_signal.assert_has_calls( buy_signal.assert_has_calls(
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] [call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]

View File

@ -1,5 +1,5 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
def test_exec_sell_order(mocker): def test_exec_sell_order(mocker):
@ -9,7 +9,7 @@ def test_exec_sell_order(mocker):
stake_amount=1.00, stake_amount=1.00,
open_rate=0.50, open_rate=0.50,
amount=10.00, amount=10.00,
exchange=Exchange.BITTREX, exchange=Exchanges.BITTREX,
open_order_id='mocked' open_order_id='mocked'
) )
profit = trade.exec_sell_order(1.00, 10.00) profit = trade.exec_sell_order(1.00, 10.00)

View File

@ -6,7 +6,6 @@ import pytest
from jsonschema import validate from jsonschema import validate
from telegram import Bot, Update, Message, Chat from telegram import Bot, Update, Message, Chat
from freqtrade import exchange
from freqtrade.main import init, create_trade from freqtrade.main import init, create_trade
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -28,7 +27,8 @@ def conf():
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },
"bittrex": { "exchange": {
"name": "bittrex",
"enabled": True, "enabled": True,
"key": "key", "key": "key",
"secret": "secret", "secret": "secret",
@ -73,7 +73,7 @@ def test_status_handle(conf, update, mocker):
init(conf, 'sqlite://') init(conf, 'sqlite://')
# Create some test data # Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX) trade = create_trade(15.0)
assert trade assert trade
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
@ -98,7 +98,7 @@ def test_profit_handle(conf, update, mocker):
init(conf, 'sqlite://') init(conf, 'sqlite://')
# Create some test data # Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX) trade = create_trade(15.0)
assert trade assert trade
trade.close_rate = 0.07256061 trade.close_rate = 0.07256061
trade.close_profit = 100.00 trade.close_profit = 100.00
@ -128,7 +128,7 @@ def test_forcesell_handle(conf, update, mocker):
init(conf, 'sqlite://') init(conf, 'sqlite://')
# Create some test data # Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX) trade = create_trade(15.0)
assert trade assert trade
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
@ -156,7 +156,7 @@ def test_performance_handle(conf, update, mocker):
init(conf, 'sqlite://') init(conf, 'sqlite://')
# Create some test data # Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX) trade = create_trade(15.0)
assert trade assert trade
trade.close_rate = 0.07256061 trade.close_rate = 0.07256061
trade.close_profit = 100.00 trade.close_profit = 100.00