Merge branch 'master' into parabolic-sar

This commit is contained in:
Janne Sinivirta 2017-09-03 16:43:20 +03:00
commit 44b9da5159
8 changed files with 180 additions and 158 deletions

View File

@ -29,7 +29,7 @@ See the example below:
"1440": 0.01, # Sell after 24 hours if there is at least 1% profit "1440": 0.01, # Sell after 24 hours if there is at least 1% profit
"720": 0.02, # Sell after 12 hours if there is at least 2% profit "720": 0.02, # Sell after 12 hours if there is at least 2% profit
"360": 0.02, # Sell after 6 hours if there is at least 2% profit "360": 0.02, # Sell after 6 hours if there is at least 2% profit
"0": 0.025 # Sell immediatly if there is at least 2.5% profit "0": 0.025 # Sell immediately if there is at least 2.5% profit
}, },
``` ```
@ -38,7 +38,7 @@ The other values should be self-explanatory,
if not feel free to raise a github issue. if not feel free to raise a github issue.
##### Prerequisites ##### Prerequisites
* python3 * python3.6
* sqlite * sqlite
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries * [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries

View File

@ -4,7 +4,7 @@ import logging
import arrow import arrow
import requests import requests
from pandas.io.json import json_normalize from pandas.io.json import json_normalize
from stockstats import StockDataFrame from pandas import DataFrame
import talib.abstract as ta import talib.abstract as ta
@ -13,11 +13,11 @@ logging.basicConfig(level=logging.DEBUG,
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_ticker_dataframe(pair): def get_ticker_dataframe(pair: str) -> DataFrame:
""" """
Analyses the trend for the given pair Analyses the trend for the given pair
:param pair: pair as str in format BTC_ETH or BTC-ETH :param pair: pair as str in format BTC_ETH or BTC-ETH
:return: StockDataFrame :return: DataFrame
""" """
minimum_date = arrow.now() - timedelta(hours=6) minimum_date = arrow.now() - timedelta(hours=6)
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks' url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
@ -41,35 +41,37 @@ def get_ticker_dataframe(pair):
'low': t['L'], 'low': t['L'],
'date': t['T'], 'date': t['T'],
} for t in sorted(data['result'], key=lambda k: k['T']) if arrow.get(t['T']) > minimum_date] } for t in sorted(data['result'], key=lambda k: k['T']) if arrow.get(t['T']) > minimum_date]
dataframe = StockDataFrame(json_normalize(data)) dataframe = DataFrame(json_normalize(data))
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.2) dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.2)
# calculate StochRSI # calculate StochRSI
window = 14 stochrsi = ta.STOCHRSI(dataframe)
rsi = dataframe['rsi_{}'.format(window)] dataframe['stochrsi'] = stochrsi['fastd'] # values between 0-100, not 0-1
rolling = rsi.rolling(window=window, center=False)
low = rolling.min() macd = ta.MACD(dataframe)
high = rolling.max() dataframe['macd'] = macd['macd']
dataframe['stochrsi'] = (rsi - low) / (high - low) dataframe['macds'] = macd['macdsignal']
dataframe['macdh'] = macd['macdhist']
return dataframe return dataframe
def populate_trends(dataframe): def populate_trends(dataframe: DataFrame) -> DataFrame:
""" """
Populates the trends for the given dataframe Populates the trends for the given dataframe
:param dataframe: StockDataFrame :param dataframe: DataFrame
:return: StockDataFrame with populated trends :return: DataFrame with populated trends
""" """
""" """
dataframe.loc[ dataframe.loc[
(dataframe['stochrsi'] < 0.20) (dataframe['stochrsi'] < 20)
& (dataframe['close_30_ema'] > (1 + 0.0025) * dataframe['close_60_ema']), & (dataframe['close_30_ema'] > (1 + 0.0025) * dataframe['close_60_ema']),
'underpriced' 'underpriced'
] = 1 ] = 1
""" """
dataframe.loc[ dataframe.loc[
(dataframe['stochrsi'] < 0.20) (dataframe['stochrsi'] < 20)
& (dataframe['macd'] > dataframe['macds']) & (dataframe['macd'] > dataframe['macds'])
& (dataframe['close'] > dataframe['sar']), & (dataframe['close'] > dataframe['sar']),
'underpriced' 'underpriced'
@ -78,7 +80,7 @@ def populate_trends(dataframe):
return dataframe return dataframe
def get_buy_signal(pair): def get_buy_signal(pair: str) -> bool:
""" """
Calculates a buy signal based on StochRSI indicator Calculates a buy signal based on StochRSI indicator
:param pair: pair in format BTC_ANT or BTC-ANT :param pair: pair in format BTC_ANT or BTC-ANT
@ -98,10 +100,10 @@ def get_buy_signal(pair):
return signal return signal
def plot_dataframe(dataframe, pair): def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
""" """
Plots the given dataframe Plots the given dataframe
:param dataframe: StockDataFrame :param dataframe: DataFrame
:param pair: pair as str :param pair: pair as str
:return: None :return: None
""" """
@ -129,8 +131,8 @@ def plot_dataframe(dataframe, pair):
ax2.legend() ax2.legend()
ax3.plot(dataframe.index.values, dataframe['stochrsi'], label='StochRSI') ax3.plot(dataframe.index.values, dataframe['stochrsi'], label='StochRSI')
ax3.plot(dataframe.index.values, [0.80] * len(dataframe.index.values)) ax3.plot(dataframe.index.values, [80] * len(dataframe.index.values))
ax3.plot(dataframe.index.values, [0.20] * len(dataframe.index.values)) ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
ax3.legend() ax3.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

View File

@ -1,5 +1,7 @@
import enum import enum
import logging import logging
from typing import List
from bittrex.bittrex import Bittrex from bittrex.bittrex import Bittrex
from poloniex import Poloniex from poloniex import Poloniex
from wrapt import synchronized from wrapt import synchronized
@ -9,18 +11,6 @@ logger = logging.getLogger(__name__)
_exchange_api = None _exchange_api = None
@synchronized
def get_exchange_api(conf):
"""
Returns the current exchange api or instantiates a new one
:return: exchange.ApiWrapper
"""
global _exchange_api
if not _exchange_api:
_exchange_api = ApiWrapper(conf)
return _exchange_api
class Exchange(enum.Enum): class Exchange(enum.Enum):
POLONIEX = 0 POLONIEX = 0
BITTREX = 1 BITTREX = 1
@ -33,9 +23,11 @@ class ApiWrapper(object):
* Bittrex * Bittrex
* Poloniex (partly) * Poloniex (partly)
""" """
def __init__(self, config): def __init__(self, config: dict):
""" """
Initializes the ApiWrapper with the given config, it does not validate those values. Initializes the ApiWrapper with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: dict :param config: dict
""" """
self.dry_run = config['dry_run'] self.dry_run = config['dry_run']
@ -53,14 +45,21 @@ class ApiWrapper(object):
self.api = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret']) self.api = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
else: else:
self.api = None self.api = None
raise RuntimeError('No exchange specified. Aborting!')
def buy(self, pair, rate, amount): # Check if all pairs are available
markets = self.get_markets()
for pair in config[self.exchange.name.lower()]['pair_whitelist']:
if pair not in markets:
raise RuntimeError('Pair {} is not available at Poloniex'.format(pair))
def buy(self, pair: str, rate: float, amount: float) -> str:
""" """
Places a limit buy order. Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH :param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order :param rate: Rate limit for order
:param amount: The amount to purchase :param amount: The amount to purchase
:return: None :return: order_id of the placed buy order
""" """
if self.dry_run: if self.dry_run:
pass pass
@ -73,7 +72,7 @@ class ApiWrapper(object):
raise RuntimeError('BITTREX: {}'.format(data['message'])) raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid'] return data['result']['uuid']
def sell(self, pair, rate, amount): def sell(self, pair: str, rate: float, amount: float) -> str:
""" """
Places a limit sell order. Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH :param pair: Pair as str, format: BTC_ETH
@ -92,7 +91,7 @@ class ApiWrapper(object):
raise RuntimeError('BITTREX: {}'.format(data['message'])) raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid'] return data['result']['uuid']
def get_balance(self, currency): def get_balance(self, currency: str) -> float:
""" """
Get account balance. Get account balance.
:param currency: currency as str, format: BTC :param currency: currency as str, format: BTC
@ -109,7 +108,7 @@ class ApiWrapper(object):
raise RuntimeError('BITTREX: {}'.format(data['message'])) raise RuntimeError('BITTREX: {}'.format(data['message']))
return float(data['result']['Balance'] or 0.0) return float(data['result']['Balance'] or 0.0)
def get_ticker(self, pair): def get_ticker(self, pair: str) -> dict:
""" """
Get Ticker for given pair. Get Ticker for given pair.
:param pair: Pair as str, format: BTC_ETC :param pair: Pair as str, format: BTC_ETC
@ -132,7 +131,7 @@ class ApiWrapper(object):
'last': float(data['result']['Last']), 'last': float(data['result']['Last']),
} }
def cancel_order(self, order_id): def cancel_order(self, order_id: str) -> None:
""" """
Cancel order for given order_id Cancel order for given order_id
:param order_id: id as str :param order_id: id as str
@ -147,7 +146,7 @@ class ApiWrapper(object):
if not data['success']: if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message'])) raise RuntimeError('BITTREX: {}'.format(data['message']))
def get_open_orders(self, pair): def get_open_orders(self, pair: str) -> List[dict]:
""" """
Get all open orders for given pair. Get all open orders for given pair.
:param pair: Pair as str, format: BTC_ETC :param pair: Pair as str, format: BTC_ETC
@ -170,7 +169,7 @@ class ApiWrapper(object):
'remaining': entry['QuantityRemaining'], 'remaining': entry['QuantityRemaining'],
} for entry in data['result']] } for entry in data['result']]
def get_pair_detail_url(self, pair): def get_pair_detail_url(self, pair: str) -> str:
""" """
Returns the market detail url for the given pair Returns the market detail url for the given pair
:param pair: pair as str, format: BTC_ANT :param pair: pair as str, format: BTC_ANT
@ -180,3 +179,29 @@ class ApiWrapper(object):
raise NotImplemented('Not implemented') raise NotImplemented('Not implemented')
elif self.exchange == Exchange.BITTREX: elif self.exchange == Exchange.BITTREX:
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-')) return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
"""
Returns all available markets
:return: list of all available pairs
"""
if self.exchange == Exchange.POLONIEX:
# TODO: implement
raise NotImplemented('Not implemented')
elif self.exchange == Exchange. BITTREX:
data = self.api.get_markets()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']]
@synchronized
def get_exchange_api(conf: dict) -> ApiWrapper:
"""
Returns the current exchange api or instantiates a new one
:return: exchange.ApiWrapper
"""
global _exchange_api
if not _exchange_api:
_exchange_api = ApiWrapper(conf)
return _exchange_api

40
main.py
View File

@ -5,11 +5,13 @@ import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from json import JSONDecodeError from json import JSONDecodeError
from typing import Optional
from requests import ConnectionError from requests import ConnectionError
from wrapt import synchronized from wrapt import synchronized
from analyze import get_buy_signal from analyze import get_buy_signal
from persistence import Trade, Session from persistence import Trade, Session
from exchange import get_exchange_api from exchange import get_exchange_api, Exchange
from rpc.telegram import TelegramHandler from rpc.telegram import TelegramHandler
from utils import get_conf from utils import get_conf
@ -32,11 +34,11 @@ class TradeThread(threading.Thread):
super().__init__() super().__init__()
self._should_stop = False self._should_stop = False
def stop(self): def stop(self) -> None:
""" stops the trader thread """ """ stops the trader thread """
self._should_stop = True self._should_stop = True
def run(self): def run(self) -> None:
""" """
Threaded main function Threaded main function
:return: None :return: None
@ -53,14 +55,14 @@ class TradeThread(threading.Thread):
finally: finally:
Session.flush() Session.flush()
time.sleep(25) time.sleep(25)
except (RuntimeError, JSONDecodeError) as e: except (RuntimeError, JSONDecodeError):
TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc())) TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
logger.exception('RuntimeError. Stopping trader ...') logger.exception('RuntimeError. Stopping trader ...')
finally: finally:
TelegramHandler.send_msg('*Status:* `Trader has stopped`') TelegramHandler.send_msg('*Status:* `Trader has stopped`')
@staticmethod @staticmethod
def _process(): def _process() -> None:
""" """
Queries the persistence layer for open trades and handles them, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. otherwise a new trade is created.
@ -69,11 +71,16 @@ class TradeThread(threading.Thread):
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < CONFIG['max_open_trades']: if len(trades) < CONFIG['max_open_trades']:
# Create entity and execute trade
try: try:
Session.add(create_trade(float(CONFIG['stake_amount']), api_wrapper.exchange)) # Create entity and execute trade
trade = create_trade(float(CONFIG['stake_amount']), api_wrapper.exchange)
if trade:
Session.add(trade)
else:
logging.info('Got no buy signal...')
except ValueError: except ValueError:
logger.exception('ValueError during trade creation') logger.exception('Unable to create trade')
for trade in trades: for trade in trades:
# Check if there is already an open order for this trade # Check if there is already an open order for this trade
orders = api_wrapper.get_open_orders(trade.pair) orders = api_wrapper.get_open_orders(trade.pair)
@ -102,8 +109,9 @@ class TradeThread(threading.Thread):
# Initial stopped TradeThread instance # Initial stopped TradeThread instance
_instance = TradeThread() _instance = TradeThread()
@synchronized @synchronized
def get_instance(recreate=False): def get_instance(recreate: bool=False) -> TradeThread:
""" """
Get the current instance of this thread. This is a singleton. Get the current instance of this thread. This is a singleton.
:param recreate: Must be True if you want to start the instance :param recreate: Must be True if you want to start the instance
@ -111,13 +119,12 @@ def get_instance(recreate=False):
""" """
global _instance global _instance
if recreate and not _instance.is_alive(): if recreate and not _instance.is_alive():
logger.debug('Creating TradeThread instance') logger.debug('Creating thread instance...')
_should_stop = False
_instance = TradeThread() _instance = TradeThread()
return _instance return _instance
def close_trade_if_fulfilled(trade): def close_trade_if_fulfilled(trade: Trade) -> bool:
""" """
Checks if the trade is closable, and if so it is being closed. Checks if the trade is closable, and if so it is being closed.
:param trade: Trade :param trade: Trade
@ -132,7 +139,7 @@ def close_trade_if_fulfilled(trade):
return False return False
def handle_trade(trade): def handle_trade(trade: Trade) -> None:
""" """
Sells the current pair if the threshold is reached and updates the trade record. Sells the current pair if the threshold is reached and updates the trade record.
:return: None :return: None
@ -173,9 +180,10 @@ def handle_trade(trade):
logger.exception('Unable to handle open order') logger.exception('Unable to handle open order')
def create_trade(stake_amount: float, exchange): def create_trade(stake_amount: float, exchange: Exchange) -> Optional[Trade]:
""" """
Creates a new trade record with a random 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
:param stake_amount: amount of btc to spend :param stake_amount: amount of btc to spend
:param exchange: exchange to use :param exchange: exchange to use
""" """
@ -203,7 +211,7 @@ def create_trade(stake_amount: float, exchange):
pair = p pair = p
break break
else: else:
raise ValueError('No buy signal from pairs: {}'.format(', '.join(whitelist))) return None
open_rate = api_wrapper.get_ticker(pair)['ask'] open_rate = api_wrapper.get_ticker(pair)['ask']
amount = stake_amount / open_rate amount = stake_amount / open_rate

View File

@ -46,7 +46,7 @@ class Trade(Base):
'closed' if not self.is_open else round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2) 'closed' if not self.is_open else round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
) )
def exec_sell_order(self, rate, amount): def exec_sell_order(self, rate: float, amount: float) -> float:
""" """
Executes a sell for the given trade and updated the entity. Executes a sell for the given trade and updated the entity.
:param rate: rate to sell for :param rate: rate to sell for

View File

@ -11,5 +11,5 @@ matplotlib==2.0.2
PYQT5==5.9 PYQT5==5.9
scikit-learn==0.19.0 scikit-learn==0.19.0
scipy==0.19.1 scipy==0.19.1
stockstats==0.2.0 jsonschema==2.6.0
TA-Lib==0.4.10 TA-Lib==0.4.10

View File

@ -1,5 +1,6 @@
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Callable, Any
import arrow import arrow
from sqlalchemy import and_, func from sqlalchemy import and_, func
@ -23,7 +24,7 @@ conf = get_conf()
api_wrapper = get_exchange_api(conf) api_wrapper = get_exchange_api(conf)
def authorized_only(command_handler): def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
""" """
Decorator to check if the message comes from the correct chat_id Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler :param command_handler: Telegram CommandHandler
@ -46,7 +47,7 @@ def authorized_only(command_handler):
class TelegramHandler(object): class TelegramHandler(object):
@staticmethod @staticmethod
@authorized_only @authorized_only
def _status(bot, update): def _status(bot: Bot, update: Update) -> None:
""" """
Handler for /status. Handler for /status.
Returns the current TradeThread status Returns the current TradeThread status
@ -97,7 +98,7 @@ class TelegramHandler(object):
@staticmethod @staticmethod
@authorized_only @authorized_only
def _profit(bot, update): def _profit(bot: Bot, update: Update) -> None:
""" """
Handler for /profit. Handler for /profit.
Returns a cumulative profit statistics. Returns a cumulative profit statistics.
@ -150,7 +151,7 @@ class TelegramHandler(object):
@staticmethod @staticmethod
@authorized_only @authorized_only
def _start(bot, update): def _start(bot: Bot, update: Update) -> None:
""" """
Handler for /start. Handler for /start.
Starts TradeThread Starts TradeThread
@ -166,7 +167,7 @@ class TelegramHandler(object):
@staticmethod @staticmethod
@authorized_only @authorized_only
def _stop(bot, update): def _stop(bot: Bot, update: Update) -> None:
""" """
Handler for /stop. Handler for /stop.
Stops TradeThread Stops TradeThread
@ -183,7 +184,7 @@ class TelegramHandler(object):
@staticmethod @staticmethod
@authorized_only @authorized_only
def _forcesell(bot, update): def _forcesell(bot: Bot, update: Update) -> None:
""" """
Handler for /forcesell <id>. Handler for /forcesell <id>.
Sells the given trade at current price Sells the given trade at current price
@ -231,7 +232,7 @@ class TelegramHandler(object):
@staticmethod @staticmethod
@authorized_only @authorized_only
def _performance(bot, update): def _performance(bot: Bot, update: Update) -> None:
""" """
Handler for /performance. Handler for /performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
@ -258,19 +259,19 @@ class TelegramHandler(object):
@staticmethod @staticmethod
@synchronized @synchronized
def get_updater(conf): def get_updater(config: dict) -> Updater:
""" """
Returns the current telegram updater instantiates a new one Returns the current telegram updater or instantiates a new one
:param conf: :param config: dict
:return: telegram.ext.Updater :return: telegram.ext.Updater
""" """
global _updater global _updater
if not _updater: if not _updater:
_updater = Updater(token=conf['telegram']['token'], workers=0) _updater = Updater(token=config['telegram']['token'], workers=0)
return _updater return _updater
@staticmethod @staticmethod
def listen(): def listen() -> None:
""" """
Registers all known command handlers and starts polling for message updates Registers all known command handlers and starts polling for message updates
:return: None :return: None
@ -286,12 +287,17 @@ class TelegramHandler(object):
] ]
for handle in handles: for handle in handles:
TelegramHandler.get_updater(conf).dispatcher.add_handler(handle) TelegramHandler.get_updater(conf).dispatcher.add_handler(handle)
TelegramHandler.get_updater(conf).start_polling(clean=True, bootstrap_retries=3) TelegramHandler.get_updater(conf).start_polling(
clean=True,
bootstrap_retries=3,
timeout=30,
read_latency=60,
)
logger.info('TelegramHandler is listening for following commands: {}' logger.info('TelegramHandler is listening for following commands: {}'
.format([h.command for h in handles])) .format([h.command for h in handles]))
@staticmethod @staticmethod
def send_msg(msg, bot=None, parse_mode=ParseMode.MARKDOWN): def send_msg(msg: str, bot: Bot=None, parse_mode: ParseMode=ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message
:param msg: message :param msg: message

145
utils.py
View File

@ -1,101 +1,82 @@
import json import json
import logging import logging
from jsonschema import validate
from wrapt import synchronized from wrapt import synchronized
from bittrex.bittrex import Bittrex
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_cur_conf = None _cur_conf = None
# Required json-schema for user specified config
_conf_schema = {
'type': 'object',
'properties': {
'max_open_trades': {'type': 'integer'},
'stake_currency': {'type': 'string'},
'stake_amount': {'type': 'number'},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
'patternProperties': {
'^[0-9.]+$': {'type': 'number'}
},
'minProperties': 1
},
'poloniex': {'$ref': '#/definitions/exchange'},
'bittrex': {'$ref': '#/definitions/exchange'},
'telegram': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'token': {'type': 'string'},
'chat_id': {'type': 'string'},
},
'required': ['enabled', 'token', 'chat_id']
}
},
'definitions': {
'exchange': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
'type': 'array',
'items': {'type': 'string'},
'uniqueItems': True
}
},
'required': ['enabled', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['poloniex']},
{'required': ['bittrex']}
],
'required': [
'max_open_trades',
'stake_currency',
'stake_amount',
'dry_run',
'minimal_roi',
'telegram'
]
}
@synchronized @synchronized
def get_conf(filename='config.json'): def get_conf(filename: str='config.json') -> dict:
""" """
Loads the config into memory and returns the instance of it Loads the config into memory validates it
and returns the singleton instance
:return: dict :return: dict
""" """
global _cur_conf global _cur_conf
if not _cur_conf: if not _cur_conf:
with open(filename) as file: with open(filename) as file:
_cur_conf = json.load(file) _cur_conf = json.load(file)
validate_conf(_cur_conf) validate(_cur_conf, _conf_schema)
return _cur_conf return _cur_conf
def validate_conf(conf):
"""
Validates if the minimal possible config is provided
:param conf: config as dict
:return: None, raises ValueError if something is wrong
"""
if not isinstance(conf.get('max_open_trades'), int):
raise ValueError('max_open_trades must be a int')
if not isinstance(conf.get('stake_currency'), str):
raise ValueError('stake_currency must be a str')
if not isinstance(conf.get('stake_amount'), float):
raise ValueError('stake_amount must be a float')
if not isinstance(conf.get('dry_run'), bool):
raise ValueError('dry_run must be a boolean')
if not isinstance(conf.get('minimal_roi'), dict):
raise ValueError('minimal_roi must be a dict')
for index, (minutes, threshold) in enumerate(conf.get('minimal_roi').items()):
if not isinstance(minutes, str):
raise ValueError('minimal_roi[{}].key must be a string'.format(index))
if not isinstance(threshold, float):
raise ValueError('minimal_roi[{}].value must be a float'.format(index))
if conf.get('telegram'):
telegram = conf.get('telegram')
if not isinstance(telegram.get('token'), str):
raise ValueError('telegram.token must be a string')
if not isinstance(telegram.get('chat_id'), str):
raise ValueError('telegram.chat_id must be a string')
if conf.get('poloniex'):
poloniex = conf.get('poloniex')
if not isinstance(poloniex.get('key'), str):
raise ValueError('poloniex.key must be a string')
if not isinstance(poloniex.get('secret'), str):
raise ValueError('poloniex.secret must be a string')
if not isinstance(poloniex.get('pair_whitelist'), list):
raise ValueError('poloniex.pair_whitelist must be a list')
if poloniex.get('enabled', False):
raise ValueError('poloniex is currently not implemented')
#if not poloniex.get('pair_whitelist'):
# raise ValueError('poloniex.pair_whitelist must contain some pairs')
if conf.get('bittrex'):
bittrex = conf.get('bittrex')
if not isinstance(bittrex.get('key'), str):
raise ValueError('bittrex.key must be a string')
if not isinstance(bittrex.get('secret'), str):
raise ValueError('bittrex.secret must be a string')
if not isinstance(bittrex.get('pair_whitelist'), list):
raise ValueError('bittrex.pair_whitelist must be a list')
if bittrex.get('enabled', False):
if not bittrex.get('pair_whitelist'):
raise ValueError('bittrex.pair_whitelist must contain some pairs')
validate_bittrex_pairs(bittrex.get('pair_whitelist'))
if conf.get('poloniex', {}).get('enabled', False) \
and conf.get('bittrex', {}).get('enabled', False):
raise ValueError('Cannot use poloniex and bittrex at the same time')
logger.info('Config is valid ...')
def validate_bittrex_pairs(pairs):
"""
Validates if all given pairs exist on bittrex
:param pairs: list of str
:return: None
"""
data = Bittrex(None, None).get_markets()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
available_markets = [market['MarketName'].replace('-', '_')for market in data['result']]
for pair in pairs:
if pair not in available_markets:
raise ValueError('Invalid pair: {}'.format(pair))