def maybe_invest(api: tradeapi.REST) -> None: target_symbols = ['AAPL', 'VOO'] user_context = UserContext() user_context.set_account(api.get_account()) stock_context_list = [ StockContext(target_symbol) for target_symbol in target_symbols ] # Populate stock context list for potential investment. for stock_context in stock_context_list: last_trade = api.get_last_trade(stock_context.symbol) stock_context.set_last_trade(last_trade) # Filter out a list of stocks that should be considered # for investment at the moment. stock_feasibility_checker = StockFeasibilityChecker(user_context) filtered_stock_context_list = filter(stock_feasibility_checker, stock_context_list) # Rank the potential list of stocks to invest based on # the potential growth. stock_comparator = StockComparator(user_context) ranked_stock_context_list = sorted(filtered_stock_context_list, key=stock_comparator) # Send order to invest if fund is sufficient stock_action_executor = StockActionExecutor(user_context) for ranked_stock_context in ranked_stock_context_list: stock_action_executor(ranked_stock_context)
def select_swing_stocks(): # Get Alpaca API key and secret storage_client = storage.Client() bucket = storage_client.get_bucket('derek-algo-trading-bucket') blob = bucket.blob('alpaca-api-key.txt') api_key = blob.download_as_text() blob = bucket.blob('alpaca-secret-key.txt') secret_key = blob.download_as_text() base_url = 'https://paper-api.alpaca.markets' api = REST(api_key, secret_key, base_url, 'v2') # Get all stocks assets = api.list_assets('active') symbols = [asset.symbol for asset in assets if asset.tradable] # Get all currently held positions positions = api.list_positions() if positions: print('Current positions:') position_symbols = [position.symbol for position in positions] position_data = {} for position in positions: current_percent = (float(position.current_price) - float(position.avg_entry_price)) \ / float(position.avg_entry_price) * 100 print('{}: {}%'.format(position.symbol, '%.2f' % current_percent)) position_data[position.symbol] = position print() # Get past 50 days data for all stocks data = {} symbols_chunked = list(chunks(list(set(symbols)), 200)) for symbol_group in symbols_chunked: print(f'Retrieving {len(symbol_group)} symbol data') data_group = api.get_barset(','.join(symbol_group), '1D', limit=1000).df for symbol in symbol_group: data[symbol] = data_group[symbol] buy_df = pd.DataFrame() sell_df = pd.DataFrame() hold_df = pd.DataFrame() c = 0 for symbol in data.keys(): df = pd.DataFrame(data[symbol]) df = df.loc[df['close'] > 0] if symbol not in position_symbols and len(df) == 1000: df['symbol'] = symbol # bullish pattern detection # bullish = [''] * len(df) # bullish[len(bullish) - 1] = pattern.detect_bullish_patterns(df) df['bullish'] = pattern.detect_bullish_patterns(df) buy_df = buy_df.append(df.loc[df.index == df.index.max()]) elif symbol in position_symbols: print(f'\nCurrently holding {symbol}:') df['symbol'] = symbol df['qty'] = int(position_data[symbol].qty) df['market_value'] = float(position_data[symbol].market_value) latest_rsi = rsi(df).df.tail(1)['rsi'][0] latest_cci = cci(df).df.tail(1)['cci'][0] purchase_price = float(position_data[symbol].avg_entry_price) latest_price = float(position_data[symbol].current_price) df['purchase_price'] = purchase_price df['latest_price'] = latest_price # If it has dropped below the entry price, GET IT OUT if latest_price < purchase_price: print( f'Price ${latest_price} is below entry ${purchase_price}: SELL' ) sell_df = sell_df.append(df.loc[df.index == df.index.max()]) # Or if the RSI or CCI are too high, GET IT OUT elif latest_rsi >= 70 or latest_cci >= 100: print(f'Overbought, RSI={latest_rsi}, CCI={latest_cci}: SELL') sell_df = sell_df.append(df.loc[df.index == df.index.max()]) # If price is at/above entry # Check if the swing is still swinging elif latest_price >= purchase_price: print( f'Price ${latest_price} is at/above entry ${purchase_price}' ) obv_df = obv(df).df obv_df['obv_ema'] = obv_df['obv'].ewm(span=20).mean() # Is the OBV still above it's EMA if obv_df.tail(1)['obv'][0] > obv_df.tail(1)['obv_ema'][0]: print('OBV is still above OBV_EMA') slope = linregress([0, 1, 2], obv_df.tail(3)['obv'].tolist()).slope if slope > 0: print( f'OBV is increasing with a slope of {slope}: HOLD') hold_df = hold_df.append( df.loc[df.index == df.index.max()]) print() c += 1 if c % 100 == 0: print(f'{c}/{len(data.keys())}') print(f'{c}/{len(data.keys())}\n') # END SCREENING SECTION # Consolidate results print('DECISION:\n') hold_stocks = hold_df.loc[hold_df.index == hold_df.index.max()] if not hold_stocks.empty: print(f'Hold {len(hold_stocks)}:\n' + '\n'.join(hold_stocks['symbol'].tolist()) + '\n') sell_stocks = sell_df.loc[sell_df.index == sell_df.index.max()] if not sell_stocks.empty: print(f'Sell {len(sell_stocks)}:\n' + '\n'.join(sell_stocks['symbol'].tolist()) + '\n') portfolio_size = 20 purchase_size = portfolio_size - len(hold_stocks) # If there's room in the portfolio to buy more if purchase_size > 0: print(f'We can purchase {purchase_size} new stocks today.') buy_stocks = buy_df.loc[buy_df.index == buy_df.index.max()] buy_stocks = buy_stocks[buy_stocks['bullish'] != ''] buy_stocks = buy_stocks.sort_values( by='volume', ascending=False).head(purchase_size) account = api.get_account() holding_value = hold_stocks['market_value'].tolist().sum( ) if not hold_stocks.empty else 0.0 available_funds = (float(account.portfolio_value) + float(account.cash) - holding_value) * 0.95 print(f'Available funds to buy: ${available_funds}') # For now this will create equal-weight positions funds_per_stock = available_funds / len(buy_stocks) # Calculate quantity to buy for each stock buy_qty = [] symbols = buy_stocks['symbol'].tolist() prices = buy_stocks['close'].tolist() for i in range(0, len(symbols)): price = prices[i] qty = math.floor(funds_per_stock / price) buy_qty.append(qty) buy_stocks['qty'] = buy_qty buy_stocks['market_value'] = buy_stocks['close'] * buy_stocks['qty'] print(f'Buy {len(buy_stocks)}:') print(buy_stocks) else: print('There is no room to buy.') buy_stocks = pd.DataFrame() return (buy_stocks, sell_stocks, hold_stocks)
class AlpacaTradeInterface(TradeInterface): def __init__(self, key, secret, endpoint): self.api = Alpaca(key, secret, endpoint) self.pending_orders = {} self.cached_tickers = {} def initialize(self, env: Environment): try: # Load cash balance account = self.api.get_account() env.get_portfolio().cash = float( account.cash) # We have margin but shouldn't use it # Load open orders orders = self.api.list_orders(status='open', limit=500) self.pending_orders = { order.id: Order(order.symbol, OrderType(order.order_type), order.qty, order.side == 'buy', order_id=order.id, limit_price=order.limit_price, stop_price=order.stop_price) for order in orders } # Load open positions positions = self.api.list_positions() for pos in positions: env.get_portfolio()._add_position( Position(pos.symbol, pos.qty, pos.avg_entry_price, pos.current_price)) except Exception: logger.exception('Failed to pull current orders and cash balance') return False return True def update(self, env: Environment): orders = self.api.list_orders(status='closed', after=datetime.now() - timedelta(days=2), limit=500) for order in orders: if order.id in self.pending_orders: logger.info(f'Order {order.id} executed') env.notify_order_completed( ExecutedOrder(order.symbol, order.filled_qty, order.filled_avg_price, order.side == 'buy')) def place_order(self, order: Order): logger.info(f'Placing order for {order.quantity} {order.ticker}') try: result = self.api.submit_order(order.ticker, order.quantity, 'buy' if order.is_buy else 'sell', order.order_type.value, 'day', limit_price=order.limit_price, stop_price=order.stop_price) order.order_id = result.id self.pending_orders[result.id] = order return True except Exception: logger.exception( f'Failed to place order for {order.quantity} {order.ticker}') return False def cancel_order(self, order_id): logger.info(f'Canceling order {order_id}') try: self.api.cancel_order(order_id) self.pending_orders.pop(order_id, None) return True except Exception: logger.exception(f'Failed to cancel order {order_id}') return False def market_open(self): clock = self.api.get_clock() return clock.is_open # TODO - we also have open/close times. May want to avoid holding overnight def open_orders(self): return [order for order in self.pending_orders.values()] def ticker_exists(self, ticker): try: if ticker in self.cached_tickers: return self.cached_tickers[ticker] asset = self.api.get_asset(ticker) self.cached_tickers[ticker] = asset.tradable return asset.tradable except: self.cached_tickers[ticker] = False return False
class AlpacaExchange(Exchange): client: REST access_key: str secret_key: str @staticmethod def build(account: Account, is_paper=False): base_url_live = "https://api.alpaca.markets" base_url_paper = "https://paper-api.alpaca.markets" return AlpacaExchange( account=account, base_url=base_url_paper if is_paper else base_url_live) def __init__(self, account: Account, base_url: str): super().__init__() self.client = REST(key_id=account.get_access_key(), secret_key=account.get_secret_key(), base_url=URL(base_url)) def get_day_candles(self, ticker: str, start: str, end: str) -> List[Candle]: pass def get_last_candle(self, ticker: str) -> Candle: snapshot = self.client.get_snapshot(symbol=ticker) return BarAdapter(ticker=ticker, bar=snapshot.minute_bar) def get_all_assets(self) -> List[Asset]: account = self.client.get_account() positions = self.client.list_positions() cash = AccountAdapter(account.__dict__.get('_raw')) stocks = [ PositionAdapter(position.__dict__.get('_raw')) for position in positions ] return [cash, *stocks] def buy(self, ticker: str, amount: Decimal) -> Optional[Order]: return self.__order(side="buy", ticker=ticker, amount=amount) def sell(self, ticker: str, volume: Decimal) -> Optional[Order]: return self.__order(side="sell", ticker=ticker, volume=volume) def get_order(self, order_id: str) -> Optional[Order]: order = self.client.get_order_by_client_order_id( client_order_id=order_id) return OrderAdapter(AlpacaOrder(**order.__dict__.get("_raw"))) def __order(self, side: str, ticker: str, amount: Decimal = None, volume: Decimal = None) -> Optional[Order]: if amount is None and volume is None: raise ValueError("order amount or volume should not be None") if not self.__is_open_now(): return None custom_order_id = str(uuid.uuid4()) qty = float(volume) if volume is not None else None notional = float(amount) if amount is not None else None order = self.client.submit_order(symbol=ticker, qty=qty, notional=notional, side=side, type="market", client_order_id=custom_order_id) return OrderAdapter(AlpacaOrder(**order.__dict__.get("_raw"))) def __is_open_now(self): """check if US exchanges (NYSE, NASDAQ, etc) are open 09:30 (EST) ~ 16:00 (EST) """ clock = self.client.get_clock() return clock.is_open