Merge branch 'master' into parabolic-sar
This commit is contained in:
commit
44b9da5159
@ -29,7 +29,7 @@ See the example below:
|
||||
"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
|
||||
"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.
|
||||
|
||||
##### Prerequisites
|
||||
* python3
|
||||
* python3.6
|
||||
* sqlite
|
||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||
|
||||
|
42
analyze.py
42
analyze.py
@ -4,7 +4,7 @@ import logging
|
||||
import arrow
|
||||
import requests
|
||||
from pandas.io.json import json_normalize
|
||||
from stockstats import StockDataFrame
|
||||
from pandas import DataFrame
|
||||
import talib.abstract as ta
|
||||
|
||||
|
||||
@ -13,11 +13,11 @@ logging.basicConfig(level=logging.DEBUG,
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_ticker_dataframe(pair):
|
||||
def get_ticker_dataframe(pair: str) -> DataFrame:
|
||||
"""
|
||||
Analyses the trend for the given pair
|
||||
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
||||
:return: StockDataFrame
|
||||
:return: DataFrame
|
||||
"""
|
||||
minimum_date = arrow.now() - timedelta(hours=6)
|
||||
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
|
||||
@ -41,35 +41,37 @@ def get_ticker_dataframe(pair):
|
||||
'low': t['L'],
|
||||
'date': t['T'],
|
||||
} 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)
|
||||
|
||||
# calculate StochRSI
|
||||
window = 14
|
||||
rsi = dataframe['rsi_{}'.format(window)]
|
||||
rolling = rsi.rolling(window=window, center=False)
|
||||
low = rolling.min()
|
||||
high = rolling.max()
|
||||
dataframe['stochrsi'] = (rsi - low) / (high - low)
|
||||
stochrsi = ta.STOCHRSI(dataframe)
|
||||
dataframe['stochrsi'] = stochrsi['fastd'] # values between 0-100, not 0-1
|
||||
|
||||
macd = ta.MACD(dataframe)
|
||||
dataframe['macd'] = macd['macd']
|
||||
dataframe['macds'] = macd['macdsignal']
|
||||
dataframe['macdh'] = macd['macdhist']
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
def populate_trends(dataframe):
|
||||
def populate_trends(dataframe: DataFrame) -> DataFrame:
|
||||
"""
|
||||
Populates the trends for the given dataframe
|
||||
:param dataframe: StockDataFrame
|
||||
:return: StockDataFrame with populated trends
|
||||
:param dataframe: DataFrame
|
||||
:return: DataFrame with populated trends
|
||||
"""
|
||||
"""
|
||||
dataframe.loc[
|
||||
(dataframe['stochrsi'] < 0.20)
|
||||
(dataframe['stochrsi'] < 20)
|
||||
& (dataframe['close_30_ema'] > (1 + 0.0025) * dataframe['close_60_ema']),
|
||||
'underpriced'
|
||||
] = 1
|
||||
"""
|
||||
dataframe.loc[
|
||||
(dataframe['stochrsi'] < 0.20)
|
||||
(dataframe['stochrsi'] < 20)
|
||||
& (dataframe['macd'] > dataframe['macds'])
|
||||
& (dataframe['close'] > dataframe['sar']),
|
||||
'underpriced'
|
||||
@ -78,7 +80,7 @@ def populate_trends(dataframe):
|
||||
return dataframe
|
||||
|
||||
|
||||
def get_buy_signal(pair):
|
||||
def get_buy_signal(pair: str) -> bool:
|
||||
"""
|
||||
Calculates a buy signal based on StochRSI indicator
|
||||
:param pair: pair in format BTC_ANT or BTC-ANT
|
||||
@ -98,10 +100,10 @@ def get_buy_signal(pair):
|
||||
return signal
|
||||
|
||||
|
||||
def plot_dataframe(dataframe, pair):
|
||||
def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
||||
"""
|
||||
Plots the given dataframe
|
||||
:param dataframe: StockDataFrame
|
||||
:param dataframe: DataFrame
|
||||
:param pair: pair as str
|
||||
:return: None
|
||||
"""
|
||||
@ -129,8 +131,8 @@ def plot_dataframe(dataframe, pair):
|
||||
ax2.legend()
|
||||
|
||||
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, [0.20] * len(dataframe.index.values))
|
||||
ax3.plot(dataframe.index.values, [80] * len(dataframe.index.values))
|
||||
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
|
||||
ax3.legend()
|
||||
|
||||
# Fine-tune figure; make subplots close to each other and hide x ticks for
|
||||
|
69
exchange.py
69
exchange.py
@ -1,5 +1,7 @@
|
||||
import enum
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from bittrex.bittrex import Bittrex
|
||||
from poloniex import Poloniex
|
||||
from wrapt import synchronized
|
||||
@ -9,18 +11,6 @@ logger = logging.getLogger(__name__)
|
||||
_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):
|
||||
POLONIEX = 0
|
||||
BITTREX = 1
|
||||
@ -33,9 +23,11 @@ class ApiWrapper(object):
|
||||
* Bittrex
|
||||
* 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
|
||||
"""
|
||||
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'])
|
||||
else:
|
||||
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.
|
||||
:param pair: Pair as str, format: BTC_ETH
|
||||
:param rate: Rate limit for order
|
||||
:param amount: The amount to purchase
|
||||
:return: None
|
||||
:return: order_id of the placed buy order
|
||||
"""
|
||||
if self.dry_run:
|
||||
pass
|
||||
@ -73,7 +72,7 @@ class ApiWrapper(object):
|
||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||
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.
|
||||
:param pair: Pair as str, format: BTC_ETH
|
||||
@ -92,7 +91,7 @@ class ApiWrapper(object):
|
||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||
return data['result']['uuid']
|
||||
|
||||
def get_balance(self, currency):
|
||||
def get_balance(self, currency: str) -> float:
|
||||
"""
|
||||
Get account balance.
|
||||
:param currency: currency as str, format: BTC
|
||||
@ -109,7 +108,7 @@ class ApiWrapper(object):
|
||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
||||
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.
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
@ -132,7 +131,7 @@ class ApiWrapper(object):
|
||||
'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
|
||||
:param order_id: id as str
|
||||
@ -147,7 +146,7 @@ class ApiWrapper(object):
|
||||
if not data['success']:
|
||||
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.
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
@ -170,7 +169,7 @@ class ApiWrapper(object):
|
||||
'remaining': entry['QuantityRemaining'],
|
||||
} 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
|
||||
:param pair: pair as str, format: BTC_ANT
|
||||
@ -180,3 +179,29 @@ class ApiWrapper(object):
|
||||
raise NotImplemented('Not implemented')
|
||||
elif self.exchange == Exchange.BITTREX:
|
||||
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
40
main.py
@ -5,11 +5,13 @@ import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from json import JSONDecodeError
|
||||
from typing import Optional
|
||||
|
||||
from requests import ConnectionError
|
||||
from wrapt import synchronized
|
||||
from analyze import get_buy_signal
|
||||
from persistence import Trade, Session
|
||||
from exchange import get_exchange_api
|
||||
from exchange import get_exchange_api, Exchange
|
||||
from rpc.telegram import TelegramHandler
|
||||
from utils import get_conf
|
||||
|
||||
@ -32,11 +34,11 @@ class TradeThread(threading.Thread):
|
||||
super().__init__()
|
||||
self._should_stop = False
|
||||
|
||||
def stop(self):
|
||||
def stop(self) -> None:
|
||||
""" stops the trader thread """
|
||||
self._should_stop = True
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Threaded main function
|
||||
:return: None
|
||||
@ -53,14 +55,14 @@ class TradeThread(threading.Thread):
|
||||
finally:
|
||||
Session.flush()
|
||||
time.sleep(25)
|
||||
except (RuntimeError, JSONDecodeError) as e:
|
||||
except (RuntimeError, JSONDecodeError):
|
||||
TelegramHandler.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
||||
logger.exception('RuntimeError. Stopping trader ...')
|
||||
finally:
|
||||
TelegramHandler.send_msg('*Status:* `Trader has stopped`')
|
||||
|
||||
@staticmethod
|
||||
def _process():
|
||||
def _process() -> None:
|
||||
"""
|
||||
Queries the persistence layer for open trades and handles them,
|
||||
otherwise a new trade is created.
|
||||
@ -69,11 +71,16 @@ class TradeThread(threading.Thread):
|
||||
# Query trades from persistence layer
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
if len(trades) < CONFIG['max_open_trades']:
|
||||
# Create entity and execute trade
|
||||
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:
|
||||
logger.exception('ValueError during trade creation')
|
||||
logger.exception('Unable to create trade')
|
||||
|
||||
for trade in trades:
|
||||
# Check if there is already an open order for this trade
|
||||
orders = api_wrapper.get_open_orders(trade.pair)
|
||||
@ -102,8 +109,9 @@ class TradeThread(threading.Thread):
|
||||
# Initial stopped TradeThread instance
|
||||
_instance = TradeThread()
|
||||
|
||||
|
||||
@synchronized
|
||||
def get_instance(recreate=False):
|
||||
def get_instance(recreate: bool=False) -> TradeThread:
|
||||
"""
|
||||
Get the current instance of this thread. This is a singleton.
|
||||
:param recreate: Must be True if you want to start the instance
|
||||
@ -111,13 +119,12 @@ def get_instance(recreate=False):
|
||||
"""
|
||||
global _instance
|
||||
if recreate and not _instance.is_alive():
|
||||
logger.debug('Creating TradeThread instance')
|
||||
_should_stop = False
|
||||
logger.debug('Creating thread instance...')
|
||||
_instance = TradeThread()
|
||||
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.
|
||||
:param trade: Trade
|
||||
@ -132,7 +139,7 @@ def close_trade_if_fulfilled(trade):
|
||||
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.
|
||||
:return: None
|
||||
@ -173,9 +180,10 @@ def handle_trade(trade):
|
||||
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 exchange: exchange to use
|
||||
"""
|
||||
@ -203,7 +211,7 @@ def create_trade(stake_amount: float, exchange):
|
||||
pair = p
|
||||
break
|
||||
else:
|
||||
raise ValueError('No buy signal from pairs: {}'.format(', '.join(whitelist)))
|
||||
return None
|
||||
|
||||
open_rate = api_wrapper.get_ticker(pair)['ask']
|
||||
amount = stake_amount / open_rate
|
||||
|
@ -46,7 +46,7 @@ class Trade(Base):
|
||||
'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.
|
||||
:param rate: rate to sell for
|
||||
|
@ -11,5 +11,5 @@ matplotlib==2.0.2
|
||||
PYQT5==5.9
|
||||
scikit-learn==0.19.0
|
||||
scipy==0.19.1
|
||||
stockstats==0.2.0
|
||||
jsonschema==2.6.0
|
||||
TA-Lib==0.4.10
|
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Callable, Any
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import and_, func
|
||||
@ -23,7 +24,7 @@ conf = get_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
|
||||
:param command_handler: Telegram CommandHandler
|
||||
@ -46,7 +47,7 @@ def authorized_only(command_handler):
|
||||
class TelegramHandler(object):
|
||||
@staticmethod
|
||||
@authorized_only
|
||||
def _status(bot, update):
|
||||
def _status(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /status.
|
||||
Returns the current TradeThread status
|
||||
@ -97,7 +98,7 @@ class TelegramHandler(object):
|
||||
|
||||
@staticmethod
|
||||
@authorized_only
|
||||
def _profit(bot, update):
|
||||
def _profit(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /profit.
|
||||
Returns a cumulative profit statistics.
|
||||
@ -150,7 +151,7 @@ class TelegramHandler(object):
|
||||
|
||||
@staticmethod
|
||||
@authorized_only
|
||||
def _start(bot, update):
|
||||
def _start(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /start.
|
||||
Starts TradeThread
|
||||
@ -166,7 +167,7 @@ class TelegramHandler(object):
|
||||
|
||||
@staticmethod
|
||||
@authorized_only
|
||||
def _stop(bot, update):
|
||||
def _stop(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /stop.
|
||||
Stops TradeThread
|
||||
@ -183,7 +184,7 @@ class TelegramHandler(object):
|
||||
|
||||
@staticmethod
|
||||
@authorized_only
|
||||
def _forcesell(bot, update):
|
||||
def _forcesell(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /forcesell <id>.
|
||||
Sells the given trade at current price
|
||||
@ -231,7 +232,7 @@ class TelegramHandler(object):
|
||||
|
||||
@staticmethod
|
||||
@authorized_only
|
||||
def _performance(bot, update):
|
||||
def _performance(bot: Bot, update: Update) -> None:
|
||||
"""
|
||||
Handler for /performance.
|
||||
Shows a performance statistic from finished trades
|
||||
@ -258,19 +259,19 @@ class TelegramHandler(object):
|
||||
|
||||
@staticmethod
|
||||
@synchronized
|
||||
def get_updater(conf):
|
||||
def get_updater(config: dict) -> Updater:
|
||||
"""
|
||||
Returns the current telegram updater instantiates a new one
|
||||
:param conf:
|
||||
Returns the current telegram updater or instantiates a new one
|
||||
:param config: dict
|
||||
:return: telegram.ext.Updater
|
||||
"""
|
||||
global _updater
|
||||
if not _updater:
|
||||
_updater = Updater(token=conf['telegram']['token'], workers=0)
|
||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||
return _updater
|
||||
|
||||
@staticmethod
|
||||
def listen():
|
||||
def listen() -> None:
|
||||
"""
|
||||
Registers all known command handlers and starts polling for message updates
|
||||
:return: None
|
||||
@ -286,12 +287,17 @@ class TelegramHandler(object):
|
||||
]
|
||||
for handle in handles:
|
||||
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: {}'
|
||||
.format([h.command for h in handles]))
|
||||
|
||||
@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
|
||||
:param msg: message
|
||||
|
145
utils.py
145
utils.py
@ -1,101 +1,82 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from jsonschema import validate
|
||||
from wrapt import synchronized
|
||||
from bittrex.bittrex import Bittrex
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_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
|
||||
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
|
||||
"""
|
||||
global _cur_conf
|
||||
if not _cur_conf:
|
||||
with open(filename) as file:
|
||||
_cur_conf = json.load(file)
|
||||
validate_conf(_cur_conf)
|
||||
validate(_cur_conf, _conf_schema)
|
||||
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))
|
||||
|
Loading…
Reference in New Issue
Block a user