diff --git a/catalyst/examples/buy_low_sell_high_live.py b/catalyst/examples/buy_low_sell_high_live.py new file mode 100644 index 00000000..a6b1ea98 --- /dev/null +++ b/catalyst/examples/buy_low_sell_high_live.py @@ -0,0 +1,156 @@ +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 +from catalyst.utils.run_algo import run_algorithm + +algo_namespace = 'buy_the_dip_live' +log = Logger('buy low sell high') + + +def initialize(context): + log.info('initializing algo') + context.ASSET_NAME = 'XRP_BTC' + context.asset = symbol(context.ASSET_NAME) + + context.TARGET_POSITIONS = 300 + context.PROFIT_TARGET = 0.1 + context.SLIPPAGE_ALLOWED = 0.02 + + 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 + + +run_algorithm( + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='bitfinex', + live=True, + algo_namespace=algo_namespace, + base_currency='btc' +) diff --git a/catalyst/exchange/bundle_utils.py b/catalyst/exchange/bundle_utils.py index d4ace25d..536baa28 100644 --- a/catalyst/exchange/bundle_utils.py +++ b/catalyst/exchange/bundle_utils.py @@ -134,21 +134,13 @@ def get_adj_dates(start, end, assets, data_frequency): if end is None or (last_entry is not None and end > last_entry): end = last_entry - if end is None: + if end is None or start >= end: raise NoDataAvailableOnExchange( exchange=asset.exchange.title(), symbol=[asset.symbol.encode('utf-8')], data_frequency=data_frequency, ) - if end is None or start >= end: - raise PricingDataBeforeTradingError( - symbols=[asset.symbol.encode('utf-8')], - exchange=asset.exchange.title(), - first_trading_day=earliest_trade, - dt=end - ) - return start, end diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index b88739da..c5884afe 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -16,7 +16,7 @@ from catalyst.exchange.exchange_bundle import ExchangeBundle from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange, \ InvalidHistoryFrequencyError, MismatchingFrequencyError, \ - BundleNotFoundError + BundleNotFoundError, NoDataAvailableOnExchange from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ ExchangeLimitOrder, ExchangeStopOrder from catalyst.exchange.exchange_portfolio import ExchangePortfolio @@ -487,11 +487,13 @@ class Exchange: data_frequency = 'daily' elif unit.lower() == 'm': - if data_frequency != 'minute': - raise MismatchingFrequencyError( - frequency=frequency, - data_frequency=data_frequency - ) + # if data_frequency != 'minute': + # raise MismatchingFrequencyError( + # frequency=frequency, + # data_frequency=data_frequency + # ) + if data_frequency == 'daily': + data_frequency = 'minute' else: raise InvalidHistoryFrequencyError(frequency) @@ -499,32 +501,41 @@ class Exchange: adj_bar_count = candle_size * bar_count start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency) - adj_start_dt, adj_end_dt = get_adj_dates( - start_dt, end_dt, assets, data_frequency - ) + try: + adj_start_dt, adj_end_dt = get_adj_dates( + start_dt, end_dt, assets, data_frequency + ) + in_bundle = True - missing_assets = self.bundle.filter_existing_assets( - assets=assets, - start_dt=adj_start_dt, - end_dt=adj_end_dt, - data_frequency=data_frequency - ) + except NoDataAvailableOnExchange: + in_bundle = False - if missing_assets: - self.bundle.ingest_assets( + if in_bundle: + missing_assets = self.bundle.filter_existing_assets( assets=assets, start_dt=adj_start_dt, end_dt=adj_end_dt, data_frequency=data_frequency ) - series = self.get_series_from_bundle( - assets=assets, - start_dt=adj_start_dt, - end_dt=adj_end_dt, - data_frequency=data_frequency, - field=field - ) + if missing_assets: + self.bundle.ingest_assets( + assets=assets, + start_dt=adj_start_dt, + end_dt=adj_end_dt, + data_frequency=data_frequency + ) + + series = self.get_series_from_bundle( + assets=assets, + start_dt=adj_start_dt, + end_dt=adj_end_dt, + data_frequency=data_frequency, + field=field + ) + + else: + series = dict() for asset in assets: if asset not in series or series[asset].index[-1] < end_dt: diff --git a/catalyst/exchange/exchange_bundle.py b/catalyst/exchange/exchange_bundle.py index 9fc5b9c9..b8f4276a 100644 --- a/catalyst/exchange/exchange_bundle.py +++ b/catalyst/exchange/exchange_bundle.py @@ -15,7 +15,7 @@ from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \ BcolzExchangeBarWriter from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \ InvalidHistoryFrequencyError, PricingDataBeforeTradingError, \ - TempBundleNotFoundError + TempBundleNotFoundError, NoDataAvailableOnExchange from catalyst.exchange.exchange_utils import get_exchange_folder from catalyst.utils.cli import maybe_show_progress from catalyst.utils.paths import ensure_directory @@ -316,7 +316,7 @@ class ExchangeBundle: asset_start, asset_end = \ get_adj_dates(start_dt, end_dt, [asset], data_frequency) - except PricingDataBeforeTradingError: + except NoDataAvailableOnExchange: continue # Aligning start / end dates with the daily calendar