diff --git a/catalyst/examples/buy_low_sell_high_live_no_interface.py b/catalyst/examples/buy_low_sell_high_live_no_interface.py new file mode 100644 index 00000000..e1459ee6 --- /dev/null +++ b/catalyst/examples/buy_low_sell_high_live_no_interface.py @@ -0,0 +1,155 @@ +''' +This algorithm requires an additional library (ta-lib) beyond those required by catalyst. +Install it first by running: +$ pip install TA-Lib + +If you get build errors like "fatal error: ta-lib/ta_libc.h: No such file or directory" +it typically means that it can't find the underlying TA-Lib library and needs to be installed. +See https://mrjbq7.github.io/ta-lib/install.html for instructions on how to install +the required dependencies. +''' + +import talib +from logbook import Logger + +from catalyst.api import ( + order, + order_target_percent, + symbol, + record, + get_open_orders, +) +from catalyst.exchange.stats_utils import get_pretty_stats + +algo_namespace = 'buy_low_sell_high_xrp' +log = Logger(algo_namespace) + + +def initialize(context): + log.info('initializing algo') + context.ASSET_NAME = 'XRP_USD' + context.asset = symbol(context.ASSET_NAME) + + context.TARGET_POSITIONS = 5000 + context.PROFIT_TARGET = 0.1 + context.SLIPPAGE_ALLOWED = 0.05 + + context.retry_check_open_orders = 10 + context.retry_update_portfolio = 10 + context.retry_order = 5 + + context.errors = [] + pass + + +def _handle_data(context, data): + prices = data.history( + context.asset, + fields='price', + bar_count=20, + frequency='15m' + ) + rsi = talib.RSI(prices.values, timeperiod=14)[-1] + log.info('got rsi: {}'.format(rsi)) + + # Buying more when RSI is low, this should lower our cost basis + if rsi <= 30: + buy_increment = 50 + elif rsi <= 40: + buy_increment = 20 + elif rsi <= 70: + buy_increment = 5 + else: + buy_increment = None + + cash = context.portfolio.cash + log.info('base currency available: {cash}'.format(cash=cash)) + + price = data.current(context.asset, 'price') + log.info('got price {price}'.format(price=price)) + + record( + price=price, + rsi=rsi, + ) + + orders = get_open_orders(context.asset) + if orders: + log.info('skipping bar until all open orders execute') + return + + is_buy = False + cost_basis = None + if context.asset in context.portfolio.positions: + position = context.portfolio.positions[context.asset] + + cost_basis = position.cost_basis + log.info( + 'found {amount} positions with cost basis {cost_basis}'.format( + amount=position.amount, + cost_basis=cost_basis + ) + ) + + if position.amount >= context.TARGET_POSITIONS: + log.info('reached positions target: {}'.format(position.amount)) + return + + if price < cost_basis: + is_buy = True + elif position.amount > 0 and \ + price > cost_basis * (1 + context.PROFIT_TARGET): + profit = (price * position.amount) - (cost_basis * position.amount) + log.info('closing position, taking profit: {}'.format(profit)) + order_target_percent( + asset=context.asset, + target=0, + limit_price=price * (1 - context.SLIPPAGE_ALLOWED), + ) + else: + log.info('no buy or sell opportunity found') + else: + is_buy = True + + if is_buy: + if buy_increment is None: + log.info('the rsi is too high to consider buying {}'.format(rsi)) + return + + if price * buy_increment > cash: + log.info('not enough base currency to consider buying') + return + + log.info( + 'buying position cheaper than cost basis {} < {}'.format( + price, + cost_basis + ) + ) + order( + asset=context.asset, + amount=buy_increment, + limit_price=price * (1 + context.SLIPPAGE_ALLOWED) + ) + + +def handle_data(context, data): + log.info('handling bar {}'.format(data.current_dt)) + # try: + _handle_data(context, data) + # except Exception as e: + # log.warn('aborting the bar on error {}'.format(e)) + # context.errors.append(e) + + log.info('completed bar {}, total execution errors {}'.format( + data.current_dt, + len(context.errors) + )) + + if len(context.errors) > 0: + log.info('the errors:\n{}'.format(context.errors)) + + +def analyze(context, stats): + log.info('the daily stats:\n{}'.format(get_pretty_stats(stats))) + pass diff --git a/catalyst/exchange/algorithm_exchange.py b/catalyst/exchange/algorithm_exchange.py index 9b797278..4fcc2517 100644 --- a/catalyst/exchange/algorithm_exchange.py +++ b/catalyst/exchange/algorithm_exchange.py @@ -61,7 +61,7 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): self.is_running = True self.retry_check_open_orders = 5 - self.retry_update_portfolio = 5 + self.retry_synchronize_portfolio = 5 self.retry_get_open_orders = 5 self.retry_order = 2 self.retry_delay = 5 @@ -175,9 +175,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): def updated_account(self): return self.exchange.account - def _update_portfolio(self, attempt_index=0): + def _synchronize_portfolio(self, attempt_index=0): try: - self.exchange.update_portfolio() + self.exchange.synchronize_portfolio() # Applying the updated last_sales_price to the positions # in the performance tracker. This seems a bit redundant @@ -195,9 +195,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): log.warn( 'update portfolio attempt {}: {}'.format(attempt_index, e) ) - if attempt_index < self.retry_update_portfolio: + if attempt_index < self.retry_synchronize_portfolio: sleep(self.retry_delay) - self._update_portfolio(attempt_index + 1) + self._synchronize_portfolio(attempt_index + 1) else: raise ExchangePortfolioDataError( data_type='update-portfolio', @@ -293,7 +293,7 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): if not self.is_running: return - self._update_portfolio() + self._synchronize_portfolio() transactions = self._check_open_orders() for transaction in transactions: diff --git a/catalyst/exchange/bittrex/bittrex.py b/catalyst/exchange/bittrex/bittrex.py index 2ac974d4..dfd1a496 100644 --- a/catalyst/exchange/bittrex/bittrex.py +++ b/catalyst/exchange/bittrex/bittrex.py @@ -25,8 +25,8 @@ class Bittrex(Exchange): self.base_currency = base_currency self._portfolio = portfolio - self.minute_writer=None - self.minute_reader=None + self.minute_writer = None + self.minute_reader = None self.assets = dict() self.load_assets() @@ -77,6 +77,7 @@ class Bittrex(Exchange): def get_balances(self): try: + log.debug('retrieving wallet balances') balances = self.api.getbalances() except Exception as e: raise ExchangeRequestError(error=e) diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index 9d37f960..33712813 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -52,7 +52,7 @@ class Exchange: self._portfolio = ExchangePortfolio( start_date=pd.Timestamp.utcnow() ) - self.update_portfolio() + self.synchronize_portfolio() return self._portfolio @@ -397,13 +397,14 @@ class Exchange: df = pd.concat(series) return df - def update_portfolio(self): + def synchronize_portfolio(self): """ Update the portfolio cash and position balances based on the latest ticker prices. :return: """ + log.debug('synchronizing portfolio with exchange {}'.format(self.name)) balances = self.get_balances() base_position_available = balances[self.base_currency] \ @@ -417,6 +418,7 @@ class Exchange: portfolio = self._portfolio portfolio.cash = base_position_available + log.debug('found base currency balance: {}'.format(portfolio.cash)) if portfolio.starting_cash is None: portfolio.starting_cash = portfolio.cash diff --git a/catalyst/utils/run_algo.py b/catalyst/utils/run_algo.py index 39693ab0..a54a00de 100644 --- a/catalyst/utils/run_algo.py +++ b/catalyst/utils/run_algo.py @@ -44,8 +44,8 @@ from catalyst.exchange.asset_finder_exchange import AssetFinderExchange from catalyst.exchange.exchange_portfolio import ExchangePortfolio from catalyst.exchange.exchange_errors import ( ExchangeRequestError, - ExchangeRequestErrorTooManyAttempts -) + ExchangeRequestErrorTooManyAttempts, + BaseCurrencyNotFoundError) from catalyst.exchange.exchange_utils import get_exchange_auth, \ get_algo_object from logbook import Logger @@ -193,7 +193,7 @@ def _run(handle_data, if live and exchange is not None: env = TradingEnvironment( environ=environ, - exchange_tz="UTC", + exchange_tz='UTC', asset_db_path=None ) env.asset_finder = AssetFinderExchange(exchange) @@ -206,32 +206,43 @@ def _run(handle_data, ) choose_loader = None - def update_portfolio(attempt_index=0): + def fetch_capital_base(attempt_index=0): """ - Fetch the portfolio for the exchange - We can't continue on error because it is required to bootstrap - the algorithm. + Fetch the base currency amount required to bootstrap + the algorithm against the exchange. + + The algorithm cannot continue without this value. + :param attempt_index: - :return: + :return capital_base: the amount of base currency available for + trading """ try: - exchange.update_portfolio() - return exchange.portfolio + log.debug('retrieving capital base in {} to bootstrap ' + 'exchange {}'.format(base_currency, exchange_name)) + balances = exchange.get_balances() except ExchangeRequestError as e: if attempt_index < 20: sleep(5) - return update_portfolio(attempt_index + 1) + return fetch_capital_base(attempt_index + 1) else: raise ExchangeRequestErrorTooManyAttempts( attempts=attempt_index, error=e ) - portfolio = update_portfolio() + if base_currency in balances: + return balances[base_currency] + else: + raise BaseCurrencyNotFoundError( + base_currency=base_currency, + exchange=exchange_name + ) + sim_params = create_simulation_parameters( start=start, end=end, - capital_base=portfolio.starting_cash, + capital_base=fetch_capital_base(), emission_rate='minute', data_frequency='minute' )