From 65c3adeab7d782da53ac92fe0d0abb8097b72336 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 9 Jun 2017 01:06:29 +0200 Subject: [PATCH 01/11] make telegram updater more robust --- rpc/telegram.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rpc/telegram.py b/rpc/telegram.py index 654570e19..64d6da866 100644 --- a/rpc/telegram.py +++ b/rpc/telegram.py @@ -286,7 +286,12 @@ 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])) From 99cbf72dc42f19c3e93e05f342cf5232a255150d Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 1 Sep 2017 20:46:01 +0200 Subject: [PATCH 02/11] adapt logging (fixes #6) --- main.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index bef1753a0..88e1d6faf 100755 --- a/main.py +++ b/main.py @@ -69,11 +69,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) @@ -175,7 +180,8 @@ def handle_trade(trade): def create_trade(stake_amount: float, exchange): """ - 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 +209,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 From 32cac115805fb8409582cd435c4bd76cd9fdd8f5 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 1 Sep 2017 21:11:46 +0200 Subject: [PATCH 03/11] add typehints --- README.md | 2 +- analyze.py | 8 ++++---- exchange.py | 44 +++++++++++++++++++++++--------------------- main.py | 19 +++++++++++-------- persistence.py | 2 +- utils.py | 7 ++++--- 6 files changed, 44 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 6479bf8ea..d6f817627 100644 --- a/README.md +++ b/README.md @@ -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 ##### Install diff --git a/analyze.py b/analyze.py index 68b7fa1c6..835fe423f 100644 --- a/analyze.py +++ b/analyze.py @@ -11,7 +11,7 @@ logging.basicConfig(level=logging.DEBUG, logger = logging.getLogger(__name__) -def get_ticker_dataframe(pair): +def get_ticker_dataframe(pair: str) -> StockDataFrame: """ Analyses the trend for the given pair :param pair: pair as str in format BTC_ETH or BTC-ETH @@ -51,7 +51,7 @@ def get_ticker_dataframe(pair): return dataframe -def populate_trends(dataframe): +def populate_trends(dataframe: StockDataFrame) -> StockDataFrame: """ Populates the trends for the given dataframe :param dataframe: StockDataFrame @@ -73,7 +73,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 @@ -93,7 +93,7 @@ def get_buy_signal(pair): return signal -def plot_dataframe(dataframe, pair): +def plot_dataframe(dataframe: StockDataFrame, pair: str) -> None: """ Plots the given dataframe :param dataframe: StockDataFrame diff --git a/exchange.py b/exchange.py index 5b4761625..d2e76e7a3 100644 --- a/exchange.py +++ b/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,7 +23,7 @@ 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. :param config: dict @@ -54,13 +44,13 @@ class ApiWrapper(object): else: self.api = None - def buy(self, pair, rate, amount): + 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 +63,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 +82,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 +99,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 +122,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 +137,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 +160,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 +170,15 @@ class ApiWrapper(object): raise NotImplemented('Not implemented') elif self.exchange == Exchange.BITTREX: return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-')) + + +@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 diff --git a/main.py b/main.py index 88e1d6faf..a17895561 100755 --- a/main.py +++ b/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 @@ -60,7 +62,7 @@ class TradeThread(threading.Thread): 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. @@ -107,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 @@ -122,7 +125,7 @@ def get_instance(recreate=False): 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 @@ -137,7 +140,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 @@ -178,7 +181,7 @@ 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]: """ Checks the implemented trading indicator(s) for a randomly picked pair, if one pair triggers the buy_signal a new trade record gets created diff --git a/persistence.py b/persistence.py index c5a1c593d..f8c954983 100644 --- a/persistence.py +++ b/persistence.py @@ -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 diff --git a/utils.py b/utils.py index e87119b5b..faff4619a 100644 --- a/utils.py +++ b/utils.py @@ -1,5 +1,6 @@ import json import logging +from typing import List from wrapt import synchronized from bittrex.bittrex import Bittrex @@ -10,7 +11,7 @@ _cur_conf = None @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 :return: dict @@ -23,7 +24,7 @@ def get_conf(filename='config.json'): return _cur_conf -def validate_conf(conf): +def validate_conf(conf: dict) -> None: """ Validates if the minimal possible config is provided :param conf: config as dict @@ -86,7 +87,7 @@ def validate_conf(conf): logger.info('Config is valid ...') -def validate_bittrex_pairs(pairs): +def validate_bittrex_pairs(pairs: List[str]) -> None: """ Validates if all given pairs exist on bittrex :param pairs: list of str From 119823eeac879205988ade354455316099960a52 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 1 Sep 2017 21:34:48 +0200 Subject: [PATCH 04/11] remove unused variables --- main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index a17895561..a97419c94 100755 --- a/main.py +++ b/main.py @@ -55,7 +55,7 @@ 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: @@ -119,8 +119,7 @@ def get_instance(recreate: bool=False) -> TradeThread: """ 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 From 360143b8cfe7487c4c3e73d0517f9a76c1e65560 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 1 Sep 2017 21:34:56 +0200 Subject: [PATCH 05/11] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6f817627..4ce7cc770 100644 --- a/README.md +++ b/README.md @@ -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 }, ``` From 6f056480872eed583d7e8716a66bd6f3c7388ee1 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 2 Sep 2017 01:09:57 +0200 Subject: [PATCH 06/11] implement get_markets --- exchange.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/exchange.py b/exchange.py index d2e76e7a3..4385073a1 100644 --- a/exchange.py +++ b/exchange.py @@ -25,7 +25,9 @@ class ApiWrapper(object): """ 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'] @@ -43,6 +45,13 @@ 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!') + + # 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: """ @@ -171,6 +180,20 @@ class ApiWrapper(object): 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: From e3eaad07b1bfb82ca4f959285b8cf866faa54758 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 2 Sep 2017 01:10:21 +0200 Subject: [PATCH 07/11] use jsonschema instead of custom type validations --- requirements.txt | 3 +- utils.py | 144 ++++++++++++++++++++--------------------------- 2 files changed, 64 insertions(+), 83 deletions(-) diff --git a/requirements.txt b/requirements.txt index 934380d1c..ca6ef974a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ matplotlib==2.0.2 PYQT5==5.9 scikit-learn==0.19.0 scipy==0.19.1 -stockstats==0.2.0 \ No newline at end of file +stockstats==0.2.0 +jsonschema==2.6.0 \ No newline at end of file diff --git a/utils.py b/utils.py index faff4619a..e7bb006fd 100644 --- a/utils.py +++ b/utils.py @@ -1,102 +1,82 @@ import json import logging -from typing import List +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: 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: dict) -> None: - """ - 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: List[str]) -> None: - """ - 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)) From aceb8007ca291578f604e577cc175e6a347fb548 Mon Sep 17 00:00:00 2001 From: gcarq Date: Sat, 2 Sep 2017 01:22:20 +0200 Subject: [PATCH 08/11] add typehints --- rpc/telegram.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/rpc/telegram.py b/rpc/telegram.py index 5ae680c2e..efb8746fa 100644 --- a/rpc/telegram.py +++ b/rpc/telegram.py @@ -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 . 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 @@ -296,7 +297,7 @@ class TelegramHandler(object): .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 From c2d7854d62c1fa492db594575b416a3dd8fbba94 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Fri, 1 Sep 2017 21:39:22 +0300 Subject: [PATCH 09/11] add TA-lib dependency --- README.md | 1 + requirements.txt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ce7cc770..e8997a017 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ if not feel free to raise a github issue. ##### Prerequisites * python3.6 * sqlite +* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries ##### Install ``` diff --git a/requirements.txt b/requirements.txt index ca6ef974a..d4c164adc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ PYQT5==5.9 scikit-learn==0.19.0 scipy==0.19.1 stockstats==0.2.0 -jsonschema==2.6.0 \ No newline at end of file +jsonschema==2.6.0 +TA-Lib==0.4.10 \ No newline at end of file From 06434385802837c12caed0b20b2087f8fd788aef Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 2 Sep 2017 11:41:30 +0300 Subject: [PATCH 10/11] use TA-lib to calculate StochRSI --- analyze.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/analyze.py b/analyze.py index 835fe423f..7acc3b311 100644 --- a/analyze.py +++ b/analyze.py @@ -5,6 +5,7 @@ import arrow import requests from pandas.io.json import json_normalize from stockstats import StockDataFrame +import talib.abstract as ta logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') @@ -42,12 +43,9 @@ def get_ticker_dataframe(pair: str) -> StockDataFrame: dataframe = StockDataFrame(json_normalize(data)) # 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 + return dataframe @@ -59,13 +57,13 @@ def populate_trends(dataframe: StockDataFrame) -> StockDataFrame: """ """ 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']), 'underpriced' ] = 1 @@ -123,8 +121,8 @@ def plot_dataframe(dataframe: StockDataFrame, pair: str) -> None: 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 From 82b90f24e7e2bb7a346a7126924bb9e243e83ca1 Mon Sep 17 00:00:00 2001 From: Janne Sinivirta Date: Sat, 2 Sep 2017 11:56:56 +0300 Subject: [PATCH 11/11] let TA-lib calculate macd, macdsignal and macdhistogram. remove the now unnecessary StockStats library --- analyze.py | 28 +++++++++++++++++----------- requirements.txt | 1 - 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/analyze.py b/analyze.py index 7acc3b311..4a38fcf0f 100644 --- a/analyze.py +++ b/analyze.py @@ -4,7 +4,8 @@ import logging import arrow import requests from pandas.io.json import json_normalize -from stockstats import StockDataFrame +from pandas import DataFrame +# from stockstats import StockDataFrame import talib.abstract as ta logging.basicConfig(level=logging.DEBUG, @@ -12,11 +13,11 @@ logging.basicConfig(level=logging.DEBUG, logger = logging.getLogger(__name__) -def get_ticker_dataframe(pair: str) -> StockDataFrame: +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' @@ -40,20 +41,25 @@ def get_ticker_dataframe(pair: str) -> StockDataFrame: '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)) # calculate StochRSI 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: StockDataFrame) -> StockDataFrame: +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[ @@ -91,10 +97,10 @@ def get_buy_signal(pair: str) -> bool: return signal -def plot_dataframe(dataframe: StockDataFrame, pair: str) -> None: +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 """ @@ -108,8 +114,8 @@ def plot_dataframe(dataframe: StockDataFrame, pair: str) -> None: fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig.suptitle(pair, fontsize=14, fontweight='bold') ax1.plot(dataframe.index.values, dataframe['close'], label='close') - ax1.plot(dataframe.index.values, dataframe['close_30_ema'], label='EMA(60)') - ax1.plot(dataframe.index.values, dataframe['close_90_ema'], label='EMA(120)') + # ax1.plot(dataframe.index.values, dataframe['close_30_ema'], label='EMA(60)') + # ax1.plot(dataframe.index.values, dataframe['close_90_ema'], label='EMA(120)') # ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell') ax1.plot(dataframe.index.values, dataframe['buy'], 'bo', label='buy') ax1.legend() diff --git a/requirements.txt b/requirements.txt index d4c164adc..fcd6c3954 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +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 \ No newline at end of file