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) ): tlog(f"{self.name} CONSIDER SHORT SELL {symbol}") if ( await super().is_sell_time(now) and position and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol) ): tlog(f"{self.name} CONSIDER SHORT BUY {symbol}") return False, {}
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]: tlog(f"ahm {symbol}") if (await super().is_buy_time(now) and not position and not open_orders.get(symbol, None) and not await self.should_cool_down(symbol, now)): tlog(f"consider buy {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 and not open_orders.get(symbol)): tlog(f"consider sell {symbol}") return False, {}
async def run( self, symbol: str, shortable: bool, position: int, now: datetime, minute_history: df, portfolio_value: float = None, debug: bool = False, backtesting: bool = False, ) -> Tuple[bool, Dict]: """ :param symbol: the symbol of the stock, :param shortable: can the stock be sold short, :param position: the current held position, :param minute_history: DataFrame holding OLHC updated per *second*, :param now: current timestamp, specially important when called from the backtester application, :param portfolio_value: your total porfolio value :param debug: true / false, should be used mostly for adding more verbosity. :param backtesting: true / false, which more are we running at :return: False w/ {} dictionary, or True w/ order execution details (see below examples) """ current_second_data = minute_history.iloc[-1] tlog(f"{symbol} data: {current_second_data}") if await super().is_buy_time(now) and not position: # # Check for buy signals?? # # # Global, cross strategies passed via the framework # target_prices[symbol] = 15.0 stop_prices[symbol] = 3.8 # # indicators *should* be filled # buy_indicators[symbol] = {"my_indicator": "random"} return ( True, { "side": "buy", "qty": str(10), "type": "limit", "limit_price": "4.4", }, ) elif (await super().is_sell_time(now) and position > 0 and last_used_strategy[symbol].name == self.name # important! ): # check if we already have open order if open_orders.get(symbol) is not None: tlog(f"{self.name}: open order for {symbol} exists, skipping") return False, {} # Check for liquidation signals sell_indicators[symbol] = {"my_indicator": "random"} tlog( f"[{self.name}] Submitting sell for {position} shares of {symbol} at {current_second_data.close}" ) return ( True, { "side": "sell", "qty": str(position), "type": "limit", "limit_price": str(current_second_data.close), }, ) return False, {}
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]: data = minute_history.iloc[-1] mama, fama = MAMA(minute_history["close"]) if mama[-1] > fama[-1]: buy_price = data.close stop_price = data.close * 0.98 target_price = data.close * 1.05 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_indicators[symbol] = { "mama": mama[-5:].tolist(), "fama": fama[-5:].tolist(), } tlog( f"[{self.name}][{now}] Submitting buy for {shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) return True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(buy_price), } if (await self.is_sell_time(now) and position and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol)): mama, fama = MAMA(minute_history["close"]) to_sell: bool = False if data.close < stop_prices[symbol]: reason = "stopped" to_sell = True elif data.close >= target_prices[symbol]: reason = "target reached" to_sell = True elif mama[-1] < fama[-1]: reason = "fama below mama" to_sell = True if to_sell: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at market {data.close} w reason {reason}" ) sell_indicators[symbol] = { "mama": mama[-5:].tolist(), "fama": fama[-5:].tolist(), "reason": reason, } return ( True, { "side": "sell", "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_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, 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) and self.was_above_vwap.get(symbol, False) and not self.traded.get(symbol, False)): # Check for buy signals lbound = config.market_open.replace(second=0, microsecond=0) close = (minute_history["close"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() open = (minute_history["open"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").first()).dropna() high = (minute_history["high"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").max()).dropna() low = (minute_history["low"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").min()).dropna() volume = (minute_history["volume"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").sum()).dropna() volume = volume[volume != 0] 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"] # calc macd on 5 min close_5min = (minute_history["close"].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() if debug: tlog( f"\n{tabulate(df[-10:], headers='keys', tablefmt='psql')}") macds = MACD(close_5min) macd = macds[0].round(3) macd_signal = macds[1].round(3) macd_hist = macds[2].round(3) vwap_series = vwap_series.round(3) close = close.round(3) to_buy = False if (not self.potential_trap.get(symbol, False) and close[-1] < vwap_series[-1] and close[-2] < vwap_series[-2] and close[-3] < vwap_series[-3] and close[-1] < open[-1] and close[-2] < open[-2] and close[-3] < open[-3] and macd[-1] < macd_signal[-1] < 0 and macd[-1] < 0 and macd_hist[-1] < macd_hist[-2] < macd_hist[-3] < 0 and minute_history["close"][-2] < minute_history["open"][-2] and minute_history["close"][lbound] < minute_history["close"][-2] < minute_history["close"][-3] < minute_history["close"][-4]): # if data.close < high_2m: # return False, {} self.potential_trap[symbol] = True self.trap_start_time[symbol] = now tlog( f"[{self.name}]:{symbol}@{now} potential short-trap {data.close}" ) return False, {} elif self.potential_trap.get(symbol, False): a_vwap = anchored_vwap(minute_history, self.trap_start_time[symbol]) if (len(a_vwap) > 10 and minute_history.close[-1] > a_vwap[-1] and minute_history.close[-2] > a_vwap[-2]): tlog( f"[{self.name}]:{symbol}@{now} crossed above anchored-vwap {data.close}" ) slope_min, _ = get_series_trend(minute_history.close[-10:]) slope_a_vwap, _ = get_series_trend(a_vwap[-10:]) if round(slope_min, 2) > round(slope_a_vwap, 2): tlog( f"[{self.name}]:{symbol}@{now} symbol slop {slope_min} above anchored-vwap slope {slope_a_vwap}" ) else: tlog( f"[{self.name}]:{symbol}@{now} anchored-vwap slope {slope_a_vwap} below symbol {slope_min}" ) return False, {} stop_price = data.close * 0.99 # a_vwap[-1] * 0.99 target_price = stop_price * 1.1 stop_prices[symbol] = round(stop_price, 2) target_prices[symbol] = round(target_price, 2) 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 = int(portfolio_value * 0.02 / data.close) if not shares_to_buy: shares_to_buy = 1 buy_price = data.close tlog( f"[{self.name}][{now}] Submitting buy for {shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) buy_indicators[symbol] = { "vwap_series": vwap_series[-5:].tolist(), "a_vwap_series": a_vwap[-5:].tolist(), "5-min-close": close[-5:].tolist(), "vwap": data.vwap, "avg": data.average, "volume": minute_history["volume"][-5:].tolist(), "slope_min": slope_min, "slope_a_vwap": slope_a_vwap, "volume_mean": minute_history["volume"][lbound:].mean(), "volume_std": minute_history["volume"][lbound:].std(ddof=0, ), } self.buy_time[symbol] = now return ( True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(buy_price), }, ) if (await super().is_sell_time(now) and position and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol)): rsi = RSI( minute_history["close"].dropna().between_time("9:30", "16:00"), 14, ) a_vwap = anchored_vwap(minute_history, self.buy_time[symbol]) sell_reasons = [] to_sell = False if data.close <= stop_prices[symbol]: to_sell = True sell_reasons.append("stopped") elif data.close >= target_prices[symbol]: to_sell = True sell_reasons.append("above target") elif rsi[-1] > 79 and data.close > latest_cost_basis[symbol]: to_sell = True sell_reasons.append("RSI maxed") elif round(data.vwap, 2) >= round(data.average, 2): # to_sell = True sell_reasons.append("crossed above vwap") elif round(data.vwap, 2) < a_vwap[-1] * 0.99: to_sell = True sell_reasons.append("crossed below a-vwap") if to_sell: sell_indicators[symbol] = { "vwap": data.vwap, "avg": data.average, "reasons": sell_reasons, "rsi": rsi[-5:].round(2).tolist(), "a_vwap": a_vwap[-5:].round(2).tolist(), } tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at market with reason:{sell_reasons}" ) return ( True, { "side": "sell", "qty": str(position), "type": "market", }, ) return False, {}
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]: data = minute_history.iloc[-1] if ( await super().is_buy_time(now) and not position and not open_orders.get(symbol, None) and data.volume > 1000 ): close = ( minute_history["close"].dropna().between_time("9:30", "16:00") ) # Check for buy signals lbound = config.market_open.replace(second=0, microsecond=0) ubound = lbound + timedelta(minutes=15) try: high_15m = minute_history[lbound:ubound]["high"].max() # type: ignore min_15m = minute_history[lbound:ubound]["low"].min() # type: ignore except Exception as e: tlog( f"{symbol}[{now}] failed to aggregate {lbound}:{ubound} {minute_history}" ) return False, {} if (high_15m - min_15m) / min_15m > 0.10: return False, {} macds = MACD(close, 13, 21) macd = macds[0] macd_signal = macds[1] macd_hist = macds[2] macd_above_signal = ( macd[-2] > macd_signal[-2] and macd[-3] > macd_signal[-3] ) hist_trending_down = macd_hist[-1] < macd_hist[-2] < macd_hist[-3] macd_treding_down = macd[-1] < macd[-2] < macd[-3] to_buy_short = False reason: List[str] = [] if ( macd_above_signal and hist_trending_down and macd_treding_down and macd[-2] > 0 and macd[-1] > 0 and data.close < data.open and data.vwap < data.open ): to_buy_short = True reason.append( "MACD positive & above signal, MACD hist trending down, MACD trended down" ) if to_buy_short: stop_price = data.close * 1.03 target_price = data.average * 0.90 target_prices[symbol] = target_price stop_prices[symbol] = stop_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 * 10 // data.close ) 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] = { "reason": reason, "macd": macd[-4:].tolist(), "macd_signal": macd_signal[-4:].tolist(), "hist_signal": macd_hist[-4:].tolist(), } return ( True, { "side": "sell", "qty": str(-shares_to_buy), "type": "market", }, ) elif ( await super().is_sell_time(now) and position and symbol in latest_cost_basis and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol) and data.volume > 1000 ): close = ( minute_history["close"].dropna().between_time("9:30", "16:00") ) macds = MACD(close, 13, 21) macd = macds[0] macd_signal = macds[1] macd_hist = macds[2] hist_change_trend = macd_hist[-2] > macd_hist[-3] below_signal = macd[-1] < macd_signal[-1] crossing_above_signal = ( macd[-1] > macd_signal[-1] and macd_signal[-2] >= macd[-2] ) to_sell = False reason = [] if crossing_above_signal: to_sell = True reason.append("MACD crossing above signal") if ( below_signal and hist_change_trend and data.close < latest_cost_basis[symbol] and macd[-1] > macd[-2] ): to_sell = True reason.append("reversing direction") elif data.close >= stop_prices[symbol]: to_sell = True reason.append("stopped") elif data.close <= target_prices[symbol]: reason.append("target reached") 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, 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]: 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 open_orders.get(symbol, None) and not await self.should_cool_down(symbol, now) and data.volume > 500): close = (minute_history["close"].dropna().between_time( "9:30", "16:00")) # calc macd on 5 min close_5min = (minute_history["close"].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() macds = MACD(close_5min, 13, 21) macd = macds[0] macd_signal = macds[1] # check if zero-crossing into negative, mark that point if macd[-1] < 0 <= macd[-2] and not self.down_cross.get( symbol, None): self.down_cross[symbol] = data.close tlog( f"{self.name}: [{now}]{symbol} identified down-ward zero-crossing of 5-min MACD w/ { self.down_cross[symbol]}" ) return False, {} elif self.down_cross.get(symbol, None) and macd[-1] >= 0: self.down_cross[symbol] = None tlog( f"{self.name}: [{now}]identified up-ward zero-crossing of 5-min MACD" ) return False, {} to_buy = False reason = [] # if passed zero crossing -> look for change in trend if (self.down_cross.get(symbol, None) and macd[-1] > macd[-2] > macd[-3] and macd[-1] > macd_signal[-1] > macd_signal[-2] and macd[-2] > macd_signal[-2]): tlog( f"{self.name}: [{now}]{symbol }identified up-ward trend {macd[-1]}, {macd[-2]}, {macd[-3]} price {data.close}" ) # check if price actually went down and not up if data.close < self.down_cross[symbol]: to_buy = True reason.append("MACD signal") if to_buy: # check RSI does not indicate overbought rsi = RSI(close, 14) if debug: tlog( f"[{self.name}][{now}] {symbol} RSI={round(rsi[-1], 2)}" ) rsi_limit = 75 if rsi[-1] < rsi_limit: if debug: tlog( f"[{self.name}][{now}] {symbol} RSI {round(rsi[-1], 2)} <= {rsi_limit}" ) else: tlog( f"[{self.name}][{now}] {symbol} RSI over-bought, cool down for 5 min" ) cool_down[symbol] = now.replace( second=0, microsecond=0) + timedelta(minutes=5) return False, {} stop_price = data.close * 0.96 target_price = self.down_cross[symbol] * 1.12 target_prices[symbol] = target_price stop_prices[symbol] = stop_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 shares_to_buy -= position if shares_to_buy > 0: self.whipsawed[symbol] = False buy_price = max(data.close, data.vwap) tlog( f"[{self.name}][{now}] Submitting buy for {shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) buy_indicators[symbol] = { "macd": macd[-5:].tolist(), "macd_signal": macd_signal[-5:].tolist(), "vwap": data.vwap, "avg": data.average, "reason": reason, } 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", }, ) elif (await super().is_sell_time(now) and position > 0 and symbol in latest_cost_basis and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol)): if (not self.whipsawed.get(symbol, None) and data.close < latest_cost_basis[symbol] * 0.99): self.whipsawed[symbol] = True serie = (minute_history["close"].dropna().between_time( "9:30", "16:00")) if data.vwap: serie[-1] = data.vwap macds = MACD( serie, 13, 21, ) 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] max_movement = ( minute_history["close"][buy_time[symbol]:].max() - 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) scalp_threshold = (target_prices[symbol] + latest_scalp_basis[symbol]) / 2.0 macd_below_signal = round(macd_val, round_factor) < round( macd_signal_val, round_factor) bail_out = ( (latest_scalp_basis[symbol] > latest_cost_basis[symbol] or (max_movement > 0.02 and max_movement > movement)) and macd_below_signal and round(macd[-1], round_factor) < round(macd[-2], round_factor)) bail_on_whipsawed = (self.whipsawed.get(symbol, False) and movement > 0.01 and macd_below_signal and round(macd[-1], round_factor) < round( macd[-2], round_factor)) scalp = movement > 0.04 or data.vwap > scalp_threshold 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 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 scalp: partial_sell = True to_sell = True sell_reasons.append("scale-out") elif bail_on_whipsawed: to_sell = True partial_sell = False limit_sell = True sell_reasons.append("bail post whipsawed") # elif macd[-1] < macd_signal[-1] <= macd_signal[-2] < macd[-2]: # to_sell = True # sell_reasons.append("MACD cross signal from above") if to_sell: 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]), } if not partial_sell: if not limit_sell: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at market with reason:{sell_reasons}" ) return ( True, { "side": "sell", "qty": str(position), "type": "market", }, ) else: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at {data.close} with reason:{sell_reasons}" ) 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}][{now}] Submitting sell for {str(qty)} shares of {symbol} at limit of {data.close }with reason:{sell_reasons}" ) return ( True, { "side": "sell", "qty": str(qty), "type": "limit", "limit_price": str(data.close), }, ) return False, {}
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)): if data.open > data.average: if debug: tlog(f"{self.name} {symbol} trending up: {data}") return False, {} lbound = config.market_open.replace(second=0, microsecond=0) close = (minute_history["close"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() open = (minute_history["open"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").first()).dropna() high = (minute_history["high"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").max()).dropna() low = (minute_history["low"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").min()).dropna() volume = (minute_history["volume"][lbound:].dropna().between_time( "9:30", "16:00").resample("5min").sum()).dropna() volume = volume[volume != 0] 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"] # calc macd on 5 min close_5min = (minute_history["close"].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() if debug: tlog( f"\n{tabulate(df[-10:], headers='keys', tablefmt='psql')}") macds = MACD(close_5min) macd = macds[0].round(3) macd_signal = macds[1].round(3) macd_hist = macds[2].round(3) vwap_series = vwap_series.round(3) close = close.round(3) if (self.was_above_vwap.get(symbol, False) and close[-1] < vwap_series[-1] and close[-2] < vwap_series[-2] and close[-3] < vwap_series[-3] and close[-1] < open[-1] and close[-2] < open[-2] and close[-3] < open[-3] and macd[-1] < macd_signal[-1] < 0 # and macd[-1] < 0 and macd_hist[-1] < macd_hist[-2] < macd_hist[-3] < 0 and data.close < data.open and data.close < minute_history["close"][-2] < minute_history["close"][-3]): stops = find_supports(data.close, minute_history, now, StopRangeType.LAST_2_HOURS) if stops: tlog( f"[self.name]:{symbol}@{data.close} potential short-trap {stops}" ) return False, {} tlog( f"\n{tabulate(df[-10:], headers='keys', tablefmt='psql')}") stop_price = vwap_series[-1] * 1.005 target_price = round( min( data.close - 10 * (stop_price - data.close), data.close * 0.98, ), 2, ) stop_prices[symbol] = round(stop_price, 2) target_prices[symbol] = round(target_price, 2) 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 = int( max( portfolio_value * config.risk // (data.close - stop_prices[symbol]), -portfolio_value * 0.02 // data.close, )) 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(), "5-min-close": close[-5:].tolist(), "vwap": data.vwap, "avg": data.average, "volume": minute_history["volume"][-5:].tolist(), "stops": [] if not stops else stops.tolist(), } self.volume_test_time[symbol] = now 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)): volume = minute_history["volume"][ self.volume_test_time[symbol]:].dropna() mu, std = norm.fit(volume) a_vwap = anchored_vwap(minute_history, self.volume_test_time[symbol]) close_5min = (minute_history["close"].dropna().between_time( "9:30", "16:00").resample("5min").last()).dropna() to_sell: bool = False reason: str = "" macds = MACD(close_5min, 13, 21) macd = macds[0].round(2) macd_signal = macds[1].round(2) macd_hist = macds[2].round(2) close_5min = close_5min.round(2) movement = (data.close - latest_cost_basis[symbol]) / latest_cost_basis[symbol] if (data.close >= stop_prices[symbol] and macd[-1] > macd_signal[-1]): to_sell = True reason = "stopped" elif data.close <= target_prices[symbol]: to_sell = True reason = "target reached" elif (close_5min[-1] > close_5min[-2] > close_5min[-3] < close_5min[-4] and data.close < latest_cost_basis[symbol]): to_sell = True reason = "reversing direction" elif (macd[-1] > macd_signal[-1] and data.close < latest_cost_basis[symbol]): to_sell = True reason = "MACD changing trend" elif (0 > macd_hist[-4] > macd_hist[-3] < macd_hist[-2] < macd_hist[-1] < 0 and data.close < latest_cost_basis[symbol]): to_sell = True reason = "MACD histogram trend reversal" elif (len(a_vwap) > 10 and minute_history.close[-1] > a_vwap[-2] and minute_history.close[-2] > a_vwap[-2]): slope_min, intercept_min, _, _, _ = linregress( range(10), minute_history.close[-10:]) slope_a_vwap, intercept_a_vwap, _, _, _ = linregress( range(10), a_vwap[-10:]) if round(slope_min, 2) > round(slope_a_vwap, 2): to_sell = True reason = f"deviate from anchored-vwap {round(slope_min, 2)}>{round(slope_a_vwap, 2)}" # elif data.volume > mu + 2 * std and data.close > data.open and data.vwap > data.open: # to_sell = True # reason = "suspicious spike in volume, may be short-trap" if to_sell: buy_indicators[symbol] = { "close_5m": close_5min[-5:].tolist(), "movement": movement, "reason": reason, "volume": minute_history.volume[-5:].tolist(), "volume fit": (mu, std), } 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, 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, 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]: data = minute_history.iloc[-1] 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 open_orders.get(symbol, None) and not await self.should_cool_down(symbol, now)): # Check for buy signals if symbol not in self.max15: lbound = config.market_open.replace(second=0, microsecond=0) ubound = lbound + timedelta(minutes=15) try: self.max15[symbol] = minute_history[lbound:ubound][ "high"].max() # type: ignore # print( # f"max={self.max15[symbol]} {lbound}, {ubound}, {minute_history}" # ) except Exception as e: tlog( f"{symbol}[{now}] failed to aggregate {lbound}:{ubound} {minute_history}" ) return False, {} if data.close > self.max15[symbol]: close = (minute_history["close"].dropna().between_time( "9:30", "16:00")) if (data.vwap < data.open and data.vwap) or data.close < data.open: if debug: tlog(f"[{self.name}][{now}] price action not positive") return False, {} macds = MACD(close) macd = macds[0].round(3) daiy_max_macd = ( macd[now.replace( # type: ignore hour=9, minute=30, second=0, microsecond=0):].between_time("9:30", "16:00").max()) macd_signal = macds[1].round(3) macd_hist = macds[2].round(3) # print(macd) macd_trending = macd[-3] < macd[-2] < macd[-1] macd_above_signal = macd[-1] > macd_signal[-1] # print(f"{symbol} {data.close} {self.max15[symbol]} {macd_signal}") macd_upper_crossover = (macd[-2] > macd_signal[-2] >= macd_signal[-3] > macd[-3]) macd_hist_trending = (macd_hist[-4] < macd_hist[-3] < macd_hist[-2] < macd_hist[-1]) to_buy = False reason = [] if (macd[-1] < 0 and macd_upper_crossover and macd_trending and macd_above_signal): to_buy = True reason.append("MACD crossover") if (macd_hist_trending and macd_hist[-3] <= 0 < macd_hist[-2] and macd[-1] < daiy_max_macd): reason.append("MACD histogram reversal") if macd[-2] > 0 >= macd[-3] and macd_trending: macd2 = MACD(close, 40, 60)[0] if macd2[-1] >= 0 and np.diff(macd2)[-1] >= 0: if (macd_hist_trending and macd_hist[-3] <= 0 < macd_hist[-2] and macd[-1] < daiy_max_macd): to_buy = True reason.append("MACD zero-cross") if debug: tlog( f"[{self.name}][{now}] slow macd confirmed trend" ) if to_buy: print("to buy!") # check RSI does not indicate overbought rsi = RSI(close, 14) if debug: tlog( f"[{self.name}][{now}] {symbol} RSI={round(rsi[-1], 2)}" ) rsi_limit = 75 if rsi[-1] < rsi_limit: if debug: tlog( f"[{self.name}][{now}] {symbol} RSI {round(rsi[-1], 2)} <= {rsi_limit}" ) else: tlog( f"[{self.name}][{now}] {symbol} RSI over-bought, cool down for 5 min" ) cool_down[symbol] = now.replace( second=0, microsecond=0) + timedelta(minutes=5) return False, {} stop_price = find_stop( data.close if not data.vwap else data.vwap, minute_history, now, ) stop_price = (stop_price - max(0.05, data.close * 0.02) if stop_price else data.close * config.default_stop) target_price = 3 * (data.close - stop_price) + data.close target_prices[symbol] = target_price stop_prices[symbol] = stop_price if portfolio_value is None: print("5555!") 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 shares_to_buy -= position if shares_to_buy > 0: self.whipsawed[symbol] = False buy_price = max(data.close, data.vwap) tlog( f"[{self.name}][{now}] Submitting buy for {shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) buy_indicators[symbol] = { "macd": macd[-5:].tolist(), "macd_signal": macd_signal[-5:].tolist(), "vwap": data.vwap, "avg": data.average, "reason": reason, "rsi": rsi[-5:].tolist(), } self.top_up[symbol] = now 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: if debug: tlog(f"[{self.name}][{now}] {data.close} < 15min high ") elif (await super().is_buy_time(now) and position and not open_orders.get(symbol, None) and not await self.should_cool_down(symbol, now) and symbol in last_used_strategy and last_used_strategy[symbol].name == self.name and data.volume > 500): if symbol not in self.top_up: return False, {} if (symbol not in latest_scalp_basis or latest_scalp_basis[symbol] > latest_cost_basis[symbol]): return False, {} if not ((now - self.top_up[symbol]).total_seconds() > timedelta(minutes=10).total_seconds()): return False, {} if (data.close > data.open and data.close > latest_scalp_basis[symbol]): close = (minute_history["close"].dropna().between_time( "9:30", "16:00")) macds = MACD(close) macd = macds[0] macd_signal = macds[1] if not (macd[-1] > macd_signal[-1] and macd[-1] > macd[-2] > macd_signal[-2]): return False, {} rsi = RSI(close, 14) if not (rsi[-2] < rsi[-1] < 75): return False, {} movement = (data.close - latest_scalp_basis[symbol] ) / latest_scalp_basis[symbol] if movement < 0.005: return False, {} shares_to_buy = int(position * 0.20) buy_price = max(data.close, data.vwap) reason = ["additional buy"] tlog( f"[{self.name}][{now}] Submitting additional buy for {shares_to_buy} shares of {symbol} at {buy_price}" ) self.top_up[symbol] = now buy_indicators[symbol] = { "macd": macd[-5:].tolist(), "macd_signal": macd_signal[-5:].tolist(), "vwap": data.vwap, "avg": data.average, "reason": reason, "rsi": rsi[-5:].tolist(), } return ( True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(buy_price), }, ) if (await super().is_sell_time(now) and position > 0 and symbol in latest_cost_basis and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol)): if (not self.whipsawed.get(symbol, None) and data.close < latest_cost_basis[symbol] * 0.99): self.whipsawed[symbol] = True serie = (minute_history["close"].dropna().between_time( "9:30", "16:00")) if data.vwap: serie[-1] = data.vwap macds = MACD( serie, 13, 21, ) macd = macds[0] macd_signal = macds[1] rsi = RSI( minute_history["close"].dropna().between_time("9:30", "16:00"), 14, ) if not latest_scalp_basis[symbol]: latest_scalp_basis[symbol] = latest_cost_basis[symbol] = 1.0 movement = (data.close - latest_scalp_basis[symbol] ) / latest_scalp_basis[symbol] max_movement = ( minute_history["close"][buy_time[symbol]:].max() - 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.01 or macd_signal_val >= 0.01 else 3) scalp_threshold = (target_prices[symbol] + latest_scalp_basis[symbol]) / 2.0 macd_below_signal = round(macd_val, round_factor) < round( macd_signal_val, round_factor) bail_out = ( (latest_scalp_basis[symbol] > latest_cost_basis[symbol] or (max_movement > 0.02 and max_movement > movement)) and macd_below_signal and round(macd[-1], round_factor) < round(macd[-2], round_factor)) bail_on_whipsawed = (self.whipsawed.get(symbol, False) and movement > 0.01 and macd_below_signal and round(macd[-1], round_factor) < round( macd[-2], round_factor)) scalp = movement > 0.04 or data.vwap > scalp_threshold 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], round_factor) < round(macd[-2], round_factor) and data.vwap < 0.95 * data.average): to_sell = True sell_reasons.append( "below cost & macd negative & RSI trending down and too far from VWAP" ) 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 and data.close > latest_cost_basis[symbol]): 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 scalp: partial_sell = True to_sell = True sell_reasons.append("scale-out") elif bail_on_whipsawed: to_sell = True partial_sell = False limit_sell = True sell_reasons.append("bail post whipsawed") elif macd[-1] < macd_signal[-1] <= macd_signal[-2] < macd[-2]: sell_reasons.append("MACD cross signal from above") if to_sell: 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]), } if not partial_sell: if not limit_sell: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at market with reason:{sell_reasons}" ) return ( True, { "side": "sell", "qty": str(position), "type": "market", }, ) else: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at {data.close} with reason:{sell_reasons}" ) 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}][{now}] Submitting sell for {str(qty)} shares of {symbol} at limit of {data.close }with reason:{sell_reasons}" ) return ( True, { "side": "sell", "qty": str(qty), "type": "limit", "limit_price": str(data.close), }, ) return False, {}
async def run( self, symbol: str, shortable: bool, position: int, now: datetime, minute_history: df, portfolio_value: float = None, debug: bool = False, backtesting: bool = False, ) -> Tuple[bool, Dict]: data = minute_history.iloc[-1] mama, fama = MAMA(minute_history["close"]) if mama[-1] > fama[-1]: buy_price = data.close stop_price = data.close * 0.98 target_price = data.close * 1.05 stop_prices[symbol] = stop_price target_prices[symbol] = target_price shares_to_buy = ((portfolio_value * config.risk // (data.close - stop_prices[symbol])) if portfolio_value else 0) if not shares_to_buy: shares_to_buy = 1 buy_indicators[symbol] = { "mama": mama[-5:].tolist(), "fama": fama[-5:].tolist(), } tlog( f"[{self.name}][{now}] Submitting buy for {shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) return True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(buy_price), } if (await self.is_sell_time(now) and position and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol)): mama, fama = MAMA(minute_history["close"]) to_sell: bool = False if data.close < stop_prices[symbol]: reason = "stopped" to_sell = True elif data.close >= target_prices[symbol]: reason = "target reached" to_sell = True elif mama[-1] < fama[-1]: reason = "fama below mama" to_sell = True if to_sell: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at market {data.close} w reason {reason}" ) sell_indicators[symbol] = { "mama": mama[-5:].tolist(), "fama": fama[-5:].tolist(), "reason": reason, } return ( True, { "side": "sell", "qty": str(position), "type": "market", }, ) return False, {}
async def run( self, symbol: str, shortable: bool, position: int, now: datetime, minute_history: df = None, portfolio_value: float = None, trading_api: tradeapi = None, debug: bool = False, backtesting: bool = False, ) -> Tuple[bool, Dict]: current_price = (minute_history.close[-1] if minute_history is not None else self.data_loader[symbol].open[now]) # tlog(f"{now} {current_price}") if (await self.is_buy_time(now) and not position and not open_orders.get(symbol, None) and not await self.should_cool_down(symbol, now)): # Calculate 7 day Bolinger Band, w 1 std if minute_history is not None: self.resampled_close[symbol] = (minute_history.between_time( "9:30", "16:00").close.resample("1D").last().dropna()) # print(self.resampled_close[symbol]) else: self.resampled_close[symbol] = ( self.data_loader[symbol].close[now - timedelta( days=30):now] # type: ignore .between_time("9:30", "16:00").resample("1D").last().dropna()) self.bband[symbol] = BBANDS( self.resampled_close[symbol], timeperiod=7, nbdevdn=1, nbdevup=1, matype=MA_Type.EMA, ) # print(self.resampled_close[symbol]) # , self.bband[symbol]) # if previous day finish below band, # and current day open above previous day close # and cross above band -> buy yesterday_lower_band = self.bband[symbol][2][-2] today_lower_band = self.bband[symbol][2][-1] yesterday_close = self.resampled_close[symbol][-2] # print( # f"yesterday_lower_band:{yesterday_lower_band} today_lower_band:{today_lower_band} yesterday_close:{yesterday_close}" # ) if minute_history is not None: start_day_index = minute_history.index.get_loc( config.market_open.replace(second=0, microsecond=0), method="nearest", ) today_open = minute_history.iloc[start_day_index].open # print("today open:", today_open) else: today_open = self.data_loader[symbol].open[ config.market_open.replace(second=0, microsecond=0)] if (yesterday_close < yesterday_lower_band and today_open > yesterday_close and current_price > today_lower_band): # check if not sell signal not triggered too # (if price pops above upper-band -> sell) yesterday_upper_band = self.bband[symbol][0][-2] if current_price > yesterday_upper_band: return False, {} print( config.market_close.replace(second=0, microsecond=0) - timedelta(days=1), self.data_loader[symbol].close[ config.market_close.replace(second=0, microsecond=0) - timedelta(days=1)], ) print( f"{now}-{yesterday_close}<{yesterday_lower_band} {today_open}>{yesterday_close} {current_price} > {today_lower_band}" ) buy_indicators[symbol] = { "lower_band": self.bband[symbol][2][-2:].tolist(), } shares_to_buy = self.amount // current_price tlog( f"[{self.name}][{now}] Submitting buy for {shares_to_buy} shares of {symbol} at {current_price}" ) tlog(f"indicators:{buy_indicators[symbol]}") return ( True, { "side": "buy", "qty": str(shares_to_buy), "type": "limit", "limit_price": str(current_price), }, ) if (await self.is_sell_time(now) and position and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol)): # Calculate 7 day Bolinger Band, w 1 std if minute_history is not None: self.resampled_close[symbol] = (minute_history.between_time( "9:30", "16:00").close.resample("1D").last().dropna()) else: self.resampled_close[symbol] = ( self.data_loader[symbol].close[now - timedelta( days=30):now] # type: ignore .between_time("9:30", "16:00").resample("1D").last().dropna()) self.bband[symbol] = BBANDS( self.resampled_close[symbol], timeperiod=7, nbdevdn=1, nbdevup=1, matype=MA_Type.EMA, ) # if price pops above upper-band -> sell yesterday_upper_band = self.bband[symbol][0][-2] # print(current_price, yesterday_upper_band) if current_price > yesterday_upper_band: sell_indicators[symbol] = { "upper_band": self.bband[symbol][0][-2:].tolist(), } tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at market" ) tlog(f"indicators:{sell_indicators[symbol]}") return ( True, { "side": "sell", "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, {}
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]: """ :param symbol: the symbol of the stock, :param shortable: can the stock be sold short, :param position: the current held position, :param minute_history: DataFrame holding OLHC updated per *second*, :param now: current timestamp, specially important when called from the backtester application, :param portfolio_value: your total porfolio value :param trading_api: the Alpca tradeapi, may either be paper or live, depending on the environment variable configurations, :param debug: true / false, should be used mostly for adding more verbosity. :param backtesting: true / false, which more are we running at :return: False w/ {} dictionary, or True w/ order execution details (see below examples) """ current_second_data = minute_history.iloc[-1] tlog(f"{symbol} data: {current_second_data}") morning_rush = ( True if (now - config.market_open).seconds // 60 < 30 else False ) if await super().is_buy_time(now) and not position: # 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: return False, {} if ( current_second_data.close > high_15m or config.bypass_market_schedule ): # # Global, cross strategies passed via the framework # target_prices[symbol] = 15.0 stop_prices[symbol] = 3.8 # # indicators *should* be filled # buy_indicators[symbol] = {"my_indicator": "random"} return ( True, { "side": "buy", "qty": str(10), "type": "limit", "limit_price": "4.4", } if not morning_rush else { "side": "buy", "qty": str(5), "type": "market", }, ) if ( await super().is_sell_time(now) and position > 0 and last_used_strategy[symbol].name == self.name # important! ): # check if we already have open order if open_orders.get(symbol) is not None: tlog(f"{self.name}: open order for {symbol} exists, skipping") return False, {} # Check for liquidation signals sell_indicators[symbol] = {"my_indicator": "random"} tlog( f"[{self.name}] Submitting sell for {position} shares of {symbol} at {current_second_data.close}" ) return ( True, { "side": "sell", "qty": str(position), "type": "limit", "limit_price": str(current_second_data.close), }, ) return False, {}
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]: 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 open_orders.get(symbol, None) and not await self.should_cool_down(symbol, now) ): # Check for buy signals lbound = config.market_open.replace(second=0, microsecond=0) ubound = lbound + timedelta(minutes=15) try: high_15m = minute_history[lbound:ubound]["high"].max() # type: ignore except Exception as e: tlog(f"{symbol}[{now}] failed to aggregate {ubound}:{lbound}") return False, {} if data.close > high_15m or ( hasattr(config, "bypass_market_schedule") and config.bypass_market_schedule ): close = ( minute_history["close"] .dropna() .between_time("9:30", "16:00") ) close_5m = ( minute_history["close"] .dropna() .between_time("9:30", "16:00") .resample("5min") .last() ).dropna() macds = MACD(close) # sell_macds = MACD(close, 13, 21) macd = macds[0] macd_signal = macds[1] macd_hist = macds[2] macd_trending = macd[-3] < macd[-2] < macd[-1] macd_above_signal = macd[-1] > macd_signal[-1] * 1.1 macd_hist_trending = ( macd_hist[-3] < macd_hist[-2] < macd_hist[-1] ) if ( macd[-1] > 0 and macd_trending and macd_above_signal and macd_hist_trending and ( data.vwap > data.open > prev_min.close and data.vwap != 0.0 or data.vwap == 0.0 and data.close > data.open > prev_min.close ) ): macd2 = MACD(close, 40, 60)[0] if macd2[-1] >= 0 and np.diff(macd2)[-1] >= 0: if debug: tlog( f"[{self.name}][{now}] slow macd confirmed trend" ) # check RSI does not indicate overbought rsi = RSI(close, 14) if debug: tlog( f"[{self.name}][{now}] {symbol} RSI={round(rsi[-1], 2)}" ) rsi_limit = 75 if rsi[-1] < rsi_limit: if debug: tlog( f"[{self.name}][{now}] {symbol} RSI {round(rsi[-1], 2)} <= {rsi_limit}" ) else: tlog( f"[{self.name}][{now}] {symbol} RSI over-bought, cool down for 5 min" ) cool_down[symbol] = now.replace( second=0, microsecond=0 ) + timedelta(minutes=5) return False, {} 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 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 shares_to_buy -= position if shares_to_buy > 0: buy_price = max(data.close, data.vwap) tlog( f"[{self.name}][{now}] Submitting buy for {shares_to_buy} shares of {symbol} at {buy_price} target {target_prices[symbol]} stop {stop_prices[symbol]}" ) buy_indicators[symbol] = { "macd": macd[-5:].tolist(), "macd_signal": macd_signal[-5:].tolist(), "vwap": data.vwap, "avg": data.average, } 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: if debug: tlog(f"[{self.name}][{now}] {data.close} < 15min high ") if ( await super().is_sell_time(now) and position > 0 and symbol in latest_cost_basis and last_used_strategy[symbol].name == self.name and not open_orders.get(symbol) ): if ( not self.whipsawed.get(symbol, None) and data.close < latest_cost_basis[symbol] * 0.98 ): self.whipsawed[symbol] = True serie = ( minute_history["close"].dropna().between_time("9:30", "16:00") ) if data.vwap: serie[-1] = data.vwap macds = MACD(serie, 13, 21,) 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 ) scalp_threshold = ( target_prices[symbol] + latest_scalp_basis[symbol] ) / 2.0 macd_below_signal = round(macd_val, round_factor) < round( macd_signal_val, round_factor ) bail_out = ( ( latest_scalp_basis[symbol] > latest_cost_basis[symbol] or movement > 0.02 ) and macd_below_signal and round(macd[-1], round_factor) < round(macd[-2], round_factor) ) bail_on_whiplash = ( data.close > latest_cost_basis[symbol] and macd_below_signal and round(macd[-1], round_factor) < round(macd[-2], round_factor) ) scalp = movement > 0.04 or data.vwap > scalp_threshold 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], round_factor) < round(macd[-2], round_factor) and data.vwap < 0.95 * data.average ): to_sell = True sell_reasons.append( "below cost & macd negative & RSI trending down and too far from VWAP" ) 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 scalp: partial_sell = True to_sell = True sell_reasons.append("scale-out") elif bail_on_whiplash: to_sell = True sell_reasons.append("bail post whiplash") if to_sell: 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] ), } if not partial_sell: if not limit_sell: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at market with reason:{sell_reasons}" ) return ( True, { "side": "sell", "qty": str(position), "type": "market", }, ) else: tlog( f"[{self.name}][{now}] Submitting sell for {position} shares of {symbol} at {data.close} with reason:{sell_reasons}" ) 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}][{now}] Submitting sell for {str(qty)} shares of {symbol} at limit of {data.close }with reason:{sell_reasons}" ) 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, {}