def find_supports( current_value, minute_history, now: datetime, range_type: StopRangeType = StopRangeType.LAST_100_MINUTES, ): # get low Series based on select time-range if range_type == StopRangeType.DAILY: series = (minute_history["low"][ts( now.replace(hour=9, minute=30, second=0, microsecond=0, tzinfo=est) ):].resample("5min").min()) elif range_type == StopRangeType.LAST_100_MINUTES: series = minute_history["low"][-100:].dropna().resample("5min").min() series = series[ts(now).floor("1D"):] elif range_type == StopRangeType.LAST_2_HOURS: series = minute_history["low"][-120:].dropna().resample("5min").min() series = series[ts(now).floor("1D"):] elif range_type == StopRangeType.LAST_3_HOURS: series = minute_history["low"][-180:].dropna().resample("5min").min() series = series[ts(now).floor("1D"):] else: raise NotImplementedError( f"stop-range type {range_type} is not implemented") # find local minima diff = np.diff(series.values) low_index = np.where((diff[:-1] <= 0) & (diff[1:] > 0))[0] + 1 if len(low_index) > 0: return [series[x] for x in low_index if series[x] < current_value] return None
def add_daily_vwap(minute_data: df, debug=False) -> bool: back_time = ts(config.market_open) try: back_time_index = minute_data["close"].index.get_loc(back_time, method="nearest") except Exception as e: if debug: tlog( f"IndexError exception {e} in add_daily_vwap for {minute_data}" ) return False minute_data["pv"] = minute_data.apply( lambda x: (x["close"] + x["high"] + x["low"]) / 3 * x["volume"], axis=1) minute_data["apv"] = minute_data["pv"][back_time_index:].cumsum() minute_data["av"] = minute_data["volume"][back_time_index:].cumsum() minute_data["average"] = minute_data["apv"] / minute_data["av"] minute_data["vwap"] = minute_data.apply( lambda x: (x["close"] + x["high"] + x["low"]) / 3, axis=1) # print(f"\n{tabulate(minute_data, headers='keys', tablefmt='psql')}") if debug: tlog( f"\n{tabulate(minute_data[-110:-100], headers='keys', tablefmt='psql')}" ) tlog( f"\n{tabulate(minute_data[-10:], headers='keys', tablefmt='psql')}" ) return True
async def find_resistances( symbol: str, strategy_name: str, current_value: float, minute_history: df, debug=False, ) -> Optional[List[float]]: """calculate supports""" est = pytz.timezone("America/New_York") back_time = ts(datetime.now(est)).to_pydatetime() - timedelta(days=3) back_time_index = minute_history["close"].index.get_loc(back_time, method="nearest") series = (minute_history["close"][back_time_index:].dropna().between_time( "9:30", "16:00").resample("15min").max()).dropna() diff = np.diff(series.values) high_index = np.where((diff[:-1] >= 0) & (diff[1:] <= 0))[0] + 1 if len(high_index) > 0: local_maximas = sorted( [series[i] for i in high_index if series[i] >= current_value]) if len(local_maximas) > 0: if debug: tlog("find_resistances()") tlog(f"{minute_history}") tlog(f"{minute_history['close'][-1]}, {series}") # tlog( # f"[{strategy_name}] find_resistances({symbol})={local_maximas}" # ) return local_maximas return None
def find_stop( current_value, minute_history, now: datetime, range_type: StopRangeType = StopRangeType.LAST_100_MINUTES, ): if range_type in (StopRangeType.DATE_RANGE, StopRangeType.WEEKLY): raise NotImplementedError( f"stop-range type {range_type} is not implemented") if range_type == StopRangeType.DAILY: series = (minute_history["low"][ts( now.replace(hour=9, minute=30, second=0, microsecond=0, tzinfo=est) ):].dropna().resample("5min").min()) else: series = minute_history["low"][-100:].dropna().resample("5min").min() series = series[ts(now).floor("1D"):] diff = np.diff(series.values) low_index = np.where((diff[:-1] <= 0) & (diff[1:] > 0))[0] + 1 if len(low_index) > 0: return series[low_index[-1]] # - max(0.05, current_value * 0.02) return None # current_value * config.default_stop
async def run( self, symbol: str, position: int, minute_history: df, now: datetime, portfolio_value: float = None, trading_api: tradeapi = None, debug: bool = False, backtesting: bool = False, ) -> Tuple[bool, Dict]: data = minute_history.iloc[-1] prev_min = minute_history.iloc[-2] morning_rush = (True if (now - config.market_open).seconds // 60 < 30 else False) if (await super().is_buy_time(now) and not position and not await self.should_cool_down(symbol, now)): # Check for buy signals lbound = config.market_open ubound = lbound + timedelta(minutes=15) if debug: tlog(f"15 schedule {lbound}/{ubound}") try: high_15m = minute_history[lbound:ubound]["high"].max( ) # type: ignore if debug: tlog(f"{minute_history[lbound:ubound]}") # type: ignore except Exception as e: # Because we're aggregating on the fly, sometimes the datetime # index can get messy until it's healed by the minute bars tlog( f"[{self.name}] error aggregation {e} - maybe should use nearest?" ) return False, {} if debug: tlog(f"15 minutes high:{high_15m}") # Get the change since yesterday's market close if data.close > high_15m or ( hasattr(config, "bypass_market_schedule") and config.bypass_market_schedule ): # and volume_today[symbol] > 30000: if debug: tlog( f"[{now}]{symbol} {data.close} above 15 minute high {high_15m}" ) last_30_max_close = minute_history[-30:]["close"].max() last_30_min_close = minute_history[-30:]["close"].min() if ((now - config.market_open).seconds // 60 > 90 and (last_30_max_close - last_30_min_close) / last_30_min_close > 0.1 and (not hasattr(config, "bypass_market_schedule") or not config.bypass_market_schedule)): tlog( f"[{self.name}][{now}] too sharp {symbol} increase in last 30 minutes, can't trust MACD, cool down for 15 minutes" ) cool_down[symbol] = now.replace( second=0, microsecond=0) + timedelta(minutes=15) return False, {} serie = (minute_history["close"].dropna().between_time( "9:30", "16:00")) if data.vwap: serie[-1] = data.vwap macds = MACD(serie) sell_macds = MACD(serie, 13, 21) macd1 = macds[0] macd_signal = macds[1] round_factor = (2 if macd1[-1] >= 0.1 or macd_signal[-1] >= 0.1 else 3) minute_shift = 0 if morning_rush or debug else -1 if debug: if macd1[-1 + minute_shift].round(round_factor) > 0: tlog(f"[{now}]{symbol} MACD > 0") if (macd1[-3 + minute_shift].round(round_factor) < macd1[-2 + minute_shift].round(round_factor) < macd1[-1 + minute_shift].round(round_factor)): tlog(f"[{now}]{symbol} MACD trending") else: tlog( f"[{now}]{symbol} MACD NOT trending -> failed {macd1[-3 + minute_shift].round(round_factor)} {macd1[-2 + minute_shift].round(round_factor)} {macd1[-1 + minute_shift].round(round_factor)}" ) if (macd1[-1 + minute_shift] > macd_signal[-1 + minute_shift]): tlog(f"[{now}]{symbol} MACD above signal") else: tlog(f"[{now}]{symbol} MACD BELOW signal -> failed") if data.close >= data.open: tlog(f"[{now}]{symbol} above open") else: tlog( f"[{now}]{symbol} close {data.close} BELOW open {data.open} -> failed" ) if (macd1[-1 + minute_shift].round(round_factor) > 0 and macd1[-3 + minute_shift].round(round_factor) < macd1[-2 + minute_shift].round(round_factor) < macd1[-1 + minute_shift].round(round_factor) and macd1[-1 + minute_shift] > macd_signal[-1 + minute_shift] and sell_macds[0][-1 + minute_shift] > 0 and data.vwap > data.open and data.close > prev_min.close and data.close > data.open): if symbol in voi and voi[symbol][-1] < 0: tlog( f"[{self.name}][{now}] Don't buy {symbol} on negative voi {voi[symbol]}" ) return False, {} if symbol in voi and voi[symbol][-1] < voi[symbol][-2]: tlog( f"[{self.name}][{now}] Don't buy {symbol} if voi not trending up {voi[symbol]}" ) return False, {} if symbol in voi: tlog( f"[{self.name}][{now}] {symbol} voi {voi[symbol]}") tlog( f"[{self.name}][{now}] MACD(12,26) for {symbol} trending up!, MACD(13,21) trending up and above signals" ) if False: # not morning_rush: back_time = ts(config.market_open) back_time_index = minute_history[ "close"].index.get_loc(back_time, method="nearest") close = ( minute_history["close"] [back_time_index:].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() open = ( minute_history["open"] [back_time_index:].dropna().between_time( "9:30", "16:00").resample("5min").first()).dropna() if close[-1] > open[-1]: tlog( f"[{now}] {symbol} confirmed 5-min candle bull" ) if debug: tlog( f"[{now}] {symbol} open={open[-5:]} close={close[-5:]}" ) else: tlog( f"[{now}] {symbol} did not confirm 5-min candle bull {close[-5:]} {open[-5:]}" ) return False, {} macd2 = MACD(serie, 40, 60)[0] # await asyncio.sleep(0) if (macd2[-1 + minute_shift] >= 0 and np.diff(macd2)[-1 + minute_shift] >= 0): tlog( f"[{self.name}][{now}] MACD(40,60) for {symbol} trending up!" ) # check RSI does not indicate overbought rsi = RSI(serie, 14) if not (rsi[-1 + minute_shift] > rsi[-2 + minute_shift] > rsi[-3 + minute_shift]): tlog( f"[{self.name}][{now}] {symbol} RSI counter MACD trend ({rsi[-1+minute_shift]},{rsi[-2+minute_shift]},{rsi[-3+minute_shift]})" ) # return False, {} # await asyncio.sleep(0) tlog( f"[{self.name}][{now}] {symbol} RSI={round(rsi[-1+minute_shift], 2)}" ) rsi_limit = 71 if not morning_rush else 80 if rsi[-1 + minute_shift] <= rsi_limit: tlog( f"[{self.name}][{now}] {symbol} RSI {round(rsi[-1+minute_shift], 2)} <= {rsi_limit}" ) enforce_resistance = ( False # (True if not morning_rush else False) ) if enforce_resistance: resistances = await find_resistances( symbol, self.name, min( data.low, prev_min.close ), # data.close if not data.vwap else data.vwap, minute_history, debug, ) supports = await find_supports( symbol, self.name, min( data.low, prev_min.close ), # data.close if not data.vwap else data.vwap, minute_history, debug, ) if resistances is None or resistances == []: tlog( f"[{self.name}] no resistance for {symbol} -> skip buy" ) cool_down[symbol] = now.replace( second=0, microsecond=0) return False, {} if supports is None or supports == []: tlog( f"[{self.name}] no supports for {symbol} -> skip buy" ) cool_down[symbol] = now.replace( second=0, microsecond=0) return False, {} next_resistance = None for potential_resistance in resistances: if potential_resistance > data.close: next_resistance = potential_resistance break if not next_resistance: tlog( f"[{self.name}] did not find resistance above {data.close}" ) return False, {} if next_resistance - data.close < 0.05: tlog( f"[{self.name}] {symbol} at price {data.close} too close to resistance {next_resistance}" ) return False, {} if data.close - supports[-1] < 0.05: tlog( f"[{self.name}] {symbol} at price {data.close} too close to support {supports[-1]} -> trend not established yet" ) return False, {} if (next_resistance - data.close) / ( data.close - supports[-1]) < 0.8: tlog( f"[{self.name}] {symbol} at price {data.close} missed entry point between support {supports[-1]} and resistance {next_resistance}" ) cool_down[symbol] = now.replace( second=0, microsecond=0) return False, {} tlog( f"[{self.name}] {symbol} at price {data.close} found entry point between support {supports[-1]} and resistance {next_resistance}" ) # Stock has passed all checks; figure out how much to buy stop_price = find_stop( data.close if not data.vwap else data.vwap, minute_history, now, ) stop_prices[symbol] = min( stop_price, supports[-1] - 0.05) target_prices[symbol] = ( data.close + (data.close - stop_prices[symbol]) * 2) symbol_resistance[symbol] = next_resistance if next_resistance - data.vwap < 0.05: tlog( f"[{self.name}] {symbol} at price {data.close} too close to resistance {next_resistance}" ) return False, {} # if data.vwap - support < 0.05: # tlog( # f"[{self.name}] {symbol} at price {data.close} too close to support {support} -> trend not established yet" # ) # return False, {} if (next_resistance - data.vwap) / ( data.vwap - stop_prices[symbol]) < 0.8: tlog( f"[{self.name}] {symbol} at price {data.close} missed entry point between support {stop_prices[symbol] } and resistance {next_resistance}" ) cool_down[symbol] = now.replace( second=0, microsecond=0) return False, {} tlog( f"[{self.name}] {symbol} at price {data.close} found entry point between support {stop_prices[symbol]} and resistance {next_resistance}" ) resistance = next_resistance support = target_prices[symbol] else: stop_price = find_stop( data.close if not data.vwap else data.vwap, minute_history, now, ) target_price = (3 * (data.close - stop_price) + data.close) target_prices[symbol] = target_price stop_prices[symbol] = stop_price resistance = target_price support = stop_price symbol_resistance[symbol] = target_price if portfolio_value is None: if trading_api: portfolio_value = float( trading_api.get_account( ).portfolio_value) else: raise Exception( "MomentumLong.run(): both portfolio_value and trading_api can't be None" ) shares_to_buy = ( portfolio_value * config.risk // (data.close - stop_prices[symbol])) if not shares_to_buy: shares_to_buy = 1 shares_to_buy -= position if shares_to_buy > 0: buy_price = max(data.close, data.vwap) tlog( f"[{self.name}] Submitting buy for {shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) # await asyncio.sleep(0) buy_indicators[symbol] = { "rsi": rsi[-1 + minute_shift].tolist(), "macd": macd1[-5 + minute_shift:].tolist(), "macd_signal": macd_signal[-5 + minute_shift:].tolist(), "slow macd": macd2[-5 + minute_shift:].tolist(), "sell_macd": sell_macds[0][-5 + minute_shift:].tolist(), "sell_macd_signal": sell_macds[1][-5 + minute_shift:].tolist(), "resistances": [resistance], "supports": [support], "vwap": data.vwap, "avg": data.average, "position_ratio": str( round( (resistance - data.vwap) / (data.vwap - support), 2, )), } if symbol in voi: buy_indicators[symbol]["voi"] = voi[symbol] return ( True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(buy_price), } if not morning_rush else { "side": "buy", "qty": str(shares_to_buy), "type": "market", }, ) else: tlog(f"[{self.name}] failed MACD(40,60) for {symbol}!") if (await super().is_sell_time(now) and position > 0 and symbol in latest_cost_basis and last_used_strategy[symbol].name == self.name): if open_orders.get(symbol) is not None: tlog( f"momentum_long: open order for {symbol} exists, skipping") return False, {} # Check for liquidation signals # Sell for a loss if it's fallen below our stop price # Sell for a loss if it's below our cost basis and MACD < 0 # Sell for a profit if it's above our target price macds = MACD( minute_history["close"].dropna().between_time("9:30", "16:00"), 13, 21, ) # await asyncio.sleep(0) macd = macds[0] macd_signal = macds[1] rsi = RSI( minute_history["close"].dropna().between_time("9:30", "16:00"), 14, ) movement = (data.close - latest_scalp_basis[symbol] ) / latest_scalp_basis[symbol] macd_val = macd[-1] macd_signal_val = macd_signal[-1] round_factor = (2 if macd_val >= 0.1 or macd_signal_val >= 0.1 else 3) # await asyncio.sleep(0) if (symbol_resistance and symbol in symbol_resistance and symbol_resistance[symbol]): scalp_threshold = (symbol_resistance[symbol] + latest_scalp_basis[symbol]) / 2.0 else: scalp_threshold = (target_prices[symbol] + latest_scalp_basis[symbol]) / 2.0 bail_threshold = (latest_scalp_basis[symbol] + scalp_threshold) / 2.0 macd_below_signal = round(macd_val, round_factor) < round( macd_signal_val, round_factor) open_rush = (False if (now - config.market_open).seconds // 60 > 45 else True) bail_out = ( # movement > min(0.02, movement_threshold) and macd_below_signal (movement > 0.01 or data.vwap > bail_threshold ) # or open_rush) and macd_below_signal and round(macd[-1], round_factor) < round(macd[-2], round_factor)) bail_on_rsi = (movement > 0.01 or data.vwap > bail_threshold) and rsi[-2] < rsi[-3] if debug and not bail_out: tlog( f"[{now}]{symbol} don't bail: data.vwap={data.vwap} bail_threshold={bail_threshold} macd_below_signal={macd_below_signal} macd[-1]={ macd[-1]} macd[-2]={macd[-2]}" ) scalp = (movement > 0.02 or data.vwap > scalp_threshold) and ( symbol not in voi or voi[symbol][-1] < voi[symbol][-2] < voi[symbol][-3]) below_cost_base = data.vwap < latest_cost_basis[symbol] rsi_limit = 79 if not morning_rush else 85 to_sell = False partial_sell = False limit_sell = False sell_reasons = [] if data.close <= stop_prices[symbol]: to_sell = True sell_reasons.append("stopped") elif (below_cost_base and round(macd_val, 2) < 0 and rsi[-1] < rsi[-2] and round(macd[-1], 2) < round(macd[-2], 2)): to_sell = True sell_reasons.append( "below cost & macd negative & RSI trending down") elif data.close >= target_prices[symbol] and macd[-1] <= 0: to_sell = True sell_reasons.append("above target & macd negative") elif rsi[-1] >= rsi_limit: to_sell = True sell_reasons.append("rsi max, cool-down for 5 minutes") cool_down[symbol] = now.replace( second=0, microsecond=0) + timedelta(minutes=5) elif bail_out: to_sell = True sell_reasons.append("bail") elif bail_on_rsi: to_sell = True sell_reasons.append("bail_on_rsi") elif scalp: partial_sell = True to_sell = True sell_reasons.append("scale-out") elif (symbol in voi and voi[symbol][-1] < 0 and voi[symbol][-1] < voi[symbol][-2] < voi[symbol][-3]): tlog(f"[{now}] {symbol} bail-on-voi identified but not acted") # to_sell = True # sell_reasons.append("bail on voi") # limit_sell = True # Check patterns if debug: tlog( f"[{now}] {symbol} min-2 = {minute_history.iloc[-2].open} {minute_history.iloc[-2].high}, {minute_history.iloc[-2].low}, {minute_history.iloc[-2].close}" ) if self.check_patterns: if (now - buy_time[symbol] > timedelta(minutes=1) and gravestone_doji( prev_min.open, prev_min.high, prev_min.low, prev_min.close, ) and data.close < data.open and data.vwap < data.open and prev_min.close > latest_cost_basis[symbol]): tlog( f"[{now}]{symbol} identified gravestone doji {prev_min.open, prev_min.high, prev_min.low, prev_min.close}" ) to_sell = True partial_sell = False sell_reasons.append("gravestone_doji") elif (now - buy_time[symbol] > timedelta(minutes=2) and spinning_top_bearish_followup( ( minute_history.iloc[-3].open, minute_history.iloc[-3].high, minute_history.iloc[-3].low, minute_history.iloc[-3].close, ), ( minute_history.iloc[-2].open, minute_history.iloc[-2].high, minute_history.iloc[-2].low, minute_history.iloc[-2].close, ), ) and data.vwap < data.open): tlog( f"[{now}] {symbol} identified bullish spinning top followed by bearish candle {(minute_history.iloc[-3].open, minute_history.iloc[-3].high,minute_history.iloc[-3].low, minute_history.iloc[-3].close), (minute_history.iloc[-2].open, minute_history.iloc[-2].high, minute_history.iloc[-2].low, minute_history.iloc[-2].close)}" ) to_sell = True partial_sell = False sell_reasons.append("bull_spinning_top_bearish_followup") elif (now - buy_time[symbol] > timedelta(minutes=2) and bullish_candle_followed_by_dragonfly( ( minute_history.iloc[-3].open, minute_history.iloc[-3].high, minute_history.iloc[-3].low, minute_history.iloc[-3].close, ), ( minute_history.iloc[-2].open, minute_history.iloc[-2].high, minute_history.iloc[-2].low, minute_history.iloc[-2].close, ), ) and data.vwap < data.open): tlog( f"[{now}] {symbol} identified bullish candle followed by dragonfly candle {(minute_history.iloc[-3].open, minute_history.iloc[-3].high,minute_history.iloc[-3].low, minute_history.iloc[-3].close), (minute_history.iloc[-2].open, minute_history.iloc[-2].high, minute_history.iloc[-2].low, minute_history.iloc[-2].close)}" ) to_sell = True partial_sell = False sell_reasons.append("bullish_candle_followed_by_dragonfly") elif (now - buy_time[symbol] > timedelta(minutes=2) and morning_rush and bearish_candle( minute_history.iloc[-3].open, minute_history.iloc[-3].high, minute_history.iloc[-3].low, minute_history.iloc[-3].close, ) and bearish_candle( minute_history.iloc[-2].open, minute_history.iloc[-2].high, minute_history.iloc[-2].low, minute_history.iloc[-2].close, ) and minute_history.iloc[-2].close < minute_history.iloc[-3].close): tlog( f"[{now}] {symbol} identified two consequtive bullish candles during morning rush{(minute_history.iloc[-3].open, minute_history.iloc[-3].high, minute_history.iloc[-3].low, minute_history.iloc[-3].close), (minute_history.iloc[-2].open, minute_history.iloc[-2].high, minute_history.iloc[-2].low, minute_history.iloc[-2].close)}" ) # to_sell = True # partial_sell = False # sell_reasons.append("two_bears_in_the_morning") if to_sell: close = minute_history["close"][-10:].dropna() open = minute_history["open"][-10:].dropna() high = minute_history["high"][-10:].dropna() low = minute_history["low"][-10:].dropna() patterns: Dict[ts, Dict[int, List[str]]] = {} pattern_functions = talib.get_function_groups( )["Pattern Recognition"] for pattern in pattern_functions: pattern_value = getattr(talib, pattern)(open, high, low, close) result = pattern_value.to_numpy().nonzero() if result[0].size > 0: for timestamp, value in pattern_value.iloc[ result].items(): t = ts(timestamp) if t not in patterns: patterns[t] = {} if value not in patterns[t]: patterns[t][value] = [pattern] else: patterns[t][value].append(pattern) candle_s = Series(patterns) candle_s = candle_s.sort_index() sell_indicators[symbol] = { "rsi": rsi[-3:].tolist(), "movement": movement, "sell_macd": macd[-5:].tolist(), "sell_macd_signal": macd_signal[-5:].tolist(), "vwap": data.vwap, "avg": data.average, "reasons": " AND ".join([str(elem) for elem in sell_reasons]), "patterns": candle_s.to_json() if candle_s.size > 0 else None, } if symbol in voi: sell_indicators[symbol]["voi"] = voi[symbol] if not partial_sell: if not limit_sell: tlog( f"[{self.name}] Submitting sell for {position} shares of {symbol} at market" ) return ( True, { "side": "sell", "qty": str(position), "type": "market", }, ) else: tlog( f"[{self.name}] Submitting sell for {position} shares of {symbol} at {data.close}" ) return ( True, { "side": "sell", "qty": str(position), "type": "limit", "limit_price": str(data.close), }, ) else: qty = int(position / 2) if position > 1 else 1 tlog( f"[{self.name}] Submitting sell for {str(qty)} shares of {symbol} at limit of {data.close}" ) return ( True, { "side": "sell", "qty": str(qty), "type": "limit", "limit_price": str(data.close), }, ) return False, {}
async def run( self, symbol: str, position: int, minute_history: df, now: datetime, portfolio_value: float = None, trading_api: tradeapi = None, debug: bool = False, backtesting: bool = False, ) -> Tuple[bool, Dict]: data = minute_history.iloc[-1] prev_minute = minute_history.iloc[-2] prev_2minutes = minute_history.iloc[-3] if await self.is_buy_time(now) and not position: # Check for buy signals back_time = ts(config.market_open) back_time_index = minute_history["close"].index.get_loc( back_time, method="nearest") close = (minute_history["close"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() open = (minute_history["open"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").first()).dropna() high = (minute_history["high"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").max()).dropna() low = (minute_history["low"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").min()).dropna() volume = (minute_history["volume"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").sum()).dropna() _df = concat( [ open.rename("open"), high.rename("high"), low.rename("low"), close.rename("close"), volume.rename("volume"), ], axis=1, ) if not add_daily_vwap(_df): tlog(f"[{now}]failed add_daily_vwap") return False, {} if debug: tlog( f"\n[{now}]{symbol} {tabulate(_df[-10:], headers='keys', tablefmt='psql')}" ) vwap_series = _df["average"] if debug: tlog( f"[{now}] {symbol} close:{round(data.close,2)} vwap:{round(vwap_series[-1],2)}" ) if len(vwap_series) < 3: tlog(f"[{now}]{symbol}: missing vwap values {vwap_series}") return False, {} if close[-2] > vwap_series[-2] and close[-1] < vwap_series[-1]: down_cross[symbol] = vwap_series.index[-1].to_pydatetime() tlog( f"[{now}] {symbol} down-crossing on 5-min bars at {down_cross[symbol]}" ) return False, {} if (close[-2] > vwap_series[-2] and close[-3] < vwap_series[-3] and data.close > prev_minute.close and data.close > data.average): if not symbol in down_cross: tlog( f"[{now}] {symbol} did not find download crossing in the past 15 min" ) return False, {} if minute_history.index[-1].to_pydatetime( ) - down_cross[symbol] > timedelta(minutes=30): tlog( f"[{now}] {symbol} down-crossing too far {down_cross[symbol]} from now" ) return False, {} stop_price = find_stop( data.close if not data.vwap else data.vwap, minute_history, now, ) target = data.close + 0.05 stop_prices[symbol] = stop_price target_prices[symbol] = target tlog( f"{symbol} found conditions for VWAP-Scalp strategy now:{now}" ) tlog( f"\n{tabulate(minute_history[-10:], headers='keys', tablefmt='psql')}" ) if portfolio_value is None: if trading_api: portfolio_value = float( trading_api.get_account().portfolio_value) else: raise Exception( "VWAPLong.run(): both portfolio_value and trading_api can't be None" ) shares_to_buy = ( portfolio_value * 20.0 * config.risk // data.close # // (data.close - stop_prices[symbol]) ) if not shares_to_buy: shares_to_buy = 1 shares_to_buy -= position if shares_to_buy > 0: tlog( f"[{self.name}] Submitting buy for {shares_to_buy} shares of {symbol} at {data.close} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) buy_indicators[symbol] = { "average": round(data.average, 2), "vwap": round(data.vwap, 2), "patterns": None, } return ( True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(data.close + 0.01), }, ) elif (await super().is_sell_time(now) and position > 0 and symbol in latest_cost_basis and last_used_strategy[symbol].name == self.name): if open_orders.get(symbol) is not None: tlog(f"vwap_scalp: open order for {symbol} exists, skipping") return False, {} to_sell = False to_sell_market = False if data.vwap <= data.average - 0.05: to_sell = True reason = "below VWAP" to_sell_market = True elif data.close >= target_prices[symbol]: to_sell = True reason = "vwap scalp" elif data.close >= target_prices[symbol]: to_sell = True reason = "vwap scalp" elif (prev_minute.close < prev_minute.open and data.close < data.open): to_sell = True reason = "vwap scalp no bears" if to_sell: sell_indicators[symbol] = { "reason": reason, "average": data.average, "vwap": data.vwap, } return ( True, { "side": "sell", "qty": str(position), "type": "market" } if to_sell_market else { "side": "sell", "qty": str(position), "type": "limit", "limit_price": str(data.close), }, ) return False, {}
def toTs(unixDate): """ Convert a single unixTimestamp to UTC time. """ return ts(unixDate, tz='utc', unit='s')
async def run( self, symbol: str, shortable: bool, position: int, minute_history: df, now: datetime, portfolio_value: float = None, trading_api: tradeapi = None, debug: bool = False, backtesting: bool = False, ) -> Tuple[bool, Dict]: if not shortable: return False, {} data = minute_history.iloc[-1] if data.close > data.average: self.was_above_vwap[symbol] = True if (await super().is_buy_time(now) and not position and not open_orders.get(symbol, None)): day_start = ts(config.market_open) try: day_start_index = minute_history["close"].index.get_loc( day_start, method="nearest") except Exception as e: tlog( f"[ERROR]{self.name}[{now}]{symbol} can't load index for {day_start} w/ {e}" ) return False, {} close = (minute_history["close"] [day_start_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() open = (minute_history["open"] [day_start_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").first()).dropna() high = (minute_history["high"] [day_start_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").max()).dropna() low = (minute_history["low"] [day_start_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").min()).dropna() volume = (minute_history["volume"] [day_start_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").sum()).dropna() df = concat( [ open.rename("open"), high.rename("high"), low.rename("low"), close.rename("close"), volume.rename("volume"), ], axis=1, ) if not add_daily_vwap(df): tlog(f"[{now}]{symbol} failed in add_daily_vwap") return False, {} vwap_series = df["average"] if (data.close < vwap_series[-1] * 0.99 and self.was_above_vwap.get(symbol, False) and close[-1] < open[-1] <= close[-2] < open[-2] <= close[-3] < open[-3]): stop_price = vwap_series[-1] target_price = data.close - 3 * (stop_price - data.close) stop_prices[symbol] = stop_price target_prices[symbol] = target_price if portfolio_value is None: if trading_api: retry = 3 while retry > 0: try: portfolio_value = float( trading_api.get_account().portfolio_value) break except ConnectionError as e: tlog( f"[{symbol}][{now}[Error] get_account() failed w/ {e}, retrying {retry} more times" ) await asyncio.sleep(0) retry -= 1 if not portfolio_value: tlog( "f[{symbol}][{now}[Error] failed to get portfolio_value" ) return False, {} else: raise Exception( f"{self.name}: both portfolio_value and trading_api can't be None" ) shares_to_buy = (portfolio_value * config.risk // (data.close - stop_prices[symbol])) if not shares_to_buy: shares_to_buy = 1 buy_price = data.close tlog( f"[{self.name}][{now}] Submitting buy short for {-shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) sell_indicators[symbol] = { "vwap_series": vwap_series[-5:].tolist(), "vwap": data.vwap, "avg": data.average, } return ( True, { "side": "sell", "qty": str(-shares_to_buy), "type": "market", }, ) if (await super().is_sell_time(now) and position and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol)): day_start = ts(config.market_open) day_start_index = minute_history["close"].index.get_loc( day_start, method="nearest") close = (minute_history["close"] [day_start_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() to_sell: bool = False reason: str = "" if data.close >= stop_prices[symbol]: to_sell = True reason = "stopped" elif data.close <= target_prices[symbol]: to_sell = True reason = "target reached" elif close[-1] > close[-2] > close[-3] < close[-4]: to_sell = True reason = "reversing direction" if to_sell: buy_indicators[symbol] = { "close_5m": close[-5:].tolist(), "reason": reason, } tlog( f"[{self.name}][{now}] Submitting sell short for {position} shares of {symbol} at market {data.close} with reason:{reason}" ) return ( True, { "side": "buy", "qty": str(-position), "type": "market", }, ) return False, {}
async def run( self, symbol: str, position: int, minute_history: df, now: datetime, portfolio_value: float = None, trading_api: tradeapi = None, debug: bool = False, backtesting: bool = False, ) -> Tuple[bool, Dict]: data = minute_history.iloc[-1] prev_minute = minute_history.iloc[-2] if await self.is_buy_time(now) and not position: # Check for buy signals lbound = config.market_open ubound = lbound + timedelta(minutes=15) try: high_15m = minute_history[lbound:ubound]["high"].max( ) # type: ignore if data.vwap < high_15m: return False, {} except Exception as e: # Because we're aggregating on the fly, sometimes the datetime # index can get messy until it's healed by the minute bars tlog( f"[{self.name}] error aggregation {e} - maybe should use nearest?" ) return False, {} back_time = ts(config.market_open) back_time_index = minute_history["close"].index.get_loc( back_time, method="nearest") close = (minute_history["close"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() open = (minute_history["open"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").first()).dropna() high = (minute_history["high"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").max()).dropna() low = (minute_history["low"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").min()).dropna() volume = (minute_history["volume"] [back_time_index:-1].dropna().between_time( "9:30", "16:00").resample("5min").sum()).dropna() _df = concat( [ open.rename("open"), high.rename("high"), low.rename("low"), close.rename("close"), volume.rename("volume"), ], axis=1, ) if not add_daily_vwap(_df): tlog(f"[{now}]failed add_daily_vwap") return False, {} if debug: tlog( f"\n[{now}]{symbol} {tabulate(_df[-10:], headers='keys', tablefmt='psql')}" ) vwap_series = _df["average"] if ( # data.vwap > close_series[-1] > close_series[-2] # and round(data.average, 2) > round(vwap_series[-1], 2) # and data.vwap > data.average # and data.low > data.average and close[-1] > vwap_series[-1] > vwap_series[-2] > low[-2] and close[-1] > high[-2] and prev_minute.close > prev_minute.open and data.close > data.open and low[-2] < vwap_series[-2] - 0.2): stop_price = find_stop( data.close if not data.vwap else data.vwap, minute_history, now, ) # upperband, middleband, lowerband = BBANDS( # minute_history["close"], timeperiod=20 # ) # stop_price = min( # prev_minute.close, # data.average - 0.01, # lowerband[-1] - 0.03, # ) target = (3 * (data.close - stop_price) + data.close ) # upperband[-1] # if target - stop_price < 0.05: # tlog( # f"{symbol} target price {target} too close to stop price {stop_price}" # ) # return False, {} # if target - data.close < 0.05: # tlog( # f"{symbol} target price {target} too close to close price {data.close}" # ) # return False, {} stop_prices[symbol] = stop_price target_prices[symbol] = target patterns: Dict[ts, Dict[int, List[str]]] = {} pattern_functions = talib.get_function_groups( )["Pattern Recognition"] for pattern in pattern_functions: pattern_value = getattr(talib, pattern)(open, high, low, close) result = pattern_value.to_numpy().nonzero() if result[0].size > 0: for timestamp, value in pattern_value.iloc[ result].items(): t = ts(timestamp) if t not in patterns: patterns[t] = {} if value not in patterns[t]: patterns[t][value] = [pattern] else: patterns[t][value].append(pattern) tlog(f"{symbol} found conditions for VWAP strategy now:{now}") candle_s = Series(patterns) candle_s = candle_s.sort_index() tlog(f"{symbol} 5-min VWAP {vwap_series}") tlog(f"{symbol} 5-min close values {close}") tlog(f"{symbol} {candle_s}") tlog( f"\n{tabulate(minute_history[-10:], headers='keys', tablefmt='psql')}" ) if candle_s.size > 0 and -100 in candle_s[-1]: tlog( f"{symbol} Bullish pattern does not exists -> should skip" ) # return False, {} if portfolio_value is None: if trading_api: portfolio_value = float( trading_api.get_account().portfolio_value) else: raise Exception( "VWAPLong.run(): both portfolio_value and trading_api can't be None" ) shares_to_buy = ( portfolio_value * 20.0 * config.risk // data.close # // (data.close - stop_prices[symbol]) ) print( f"shares to buy {shares_to_buy} {data.close} {stop_prices[symbol]}" ) if not shares_to_buy: shares_to_buy = 1 shares_to_buy -= position if shares_to_buy > 0: tlog( f"[{self.name}] Submitting buy for {shares_to_buy} shares of {symbol} at {data.close} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) buy_indicators[symbol] = { # bbrand_lower": lowerband[-5:].tolist(), # "bbrand_middle": middleband[-5:].tolist(), # "bbrand_upper": upperband[-5:].tolist(), "average": round(data.average, 2), "vwap": round(data.vwap, 2), "patterns": candle_s.to_json(), } return ( True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(data.close), }, ) elif debug: tlog(f"[{now}]{symbol} failed vwap strategy") if not (data.low > data.average): tlog( f"[{now}]{symbol} failed data.low {data.low} > data.average {data.average}" ) if not (close[-1] > vwap_series[-1] > vwap_series[-2] > low[-2]): tlog( f"[{now}]{symbol} failed close[-1] {close[-1]} > vwap_series[-1] {vwap_series[-1]} > vwap_series[-2]{ vwap_series[-2]} > low[-2] {low[-2]}" ) if not (prev_minute.close > prev_minute.open): tlog( f"[{now}]{symbol} failed prev_minute.close {prev_minute.close} > prev_minute.open {prev_minute.open}" ) if not (close[-1] > high[-2]): tlog( f"[{now}]{symbol} failed close[-1] {close[-1]} > high[-2] {high[-2]}" ) if not (data.close > data.open): tlog( f"[{now}]{symbol} failed data.close {data.close} > data.open {data.open}" ) if not low[-2] < vwap_series[-2] - 0.2: tlog( f"[{now}]{symbol} failed low[-2] {low[-2]} < vwap_series[-2] {vwap_series[-2] } - 0.2" ) elif (await super().is_sell_time(now) and position > 0 and symbol in latest_cost_basis and last_used_strategy[symbol].name == self.name): if open_orders.get(symbol) is not None: tlog(f"vwap_long: open order for {symbol} exists, skipping") return False, {} if data.vwap <= data.average - 0.02: sell_indicators[symbol] = { "reason": "below VWAP", "average": data.average, "vwap": data.vwap, } return ( True, { "side": "sell", "qty": str(position), "type": "market" }, ) return False, {}