def get_historical_data_from_poylgon_for_symbols( api: tradeapi, symbols: List[str], start_date: date, end_date: date ) -> Dict[str, df]: minute_history = {} for symbol in symbols: if symbol not in minute_history: minute_history[symbol] = api.polygon.historic_agg_v2( symbol, 1, "minute", _from=start_date, to=end_date, ).df.tz_convert("US/Eastern") add_daily_vwap(minute_history[symbol]) return minute_history
async def backtest_symbol( symbol: str, scanner_start_time: datetime ) -> None: est = pytz.timezone("America/New_York") scanner_start_time = ( pytz.utc.localize(scanner_start_time).astimezone(est) if scanner_start_time.tzinfo is None else scanner_start_time ) start_time = pytz.utc.localize(start).astimezone(est) if scanner_start_time > start_time + duration: print( f"{symbol} picked too late at {scanner_start_time} ({start_time}, {duration})" ) return start_time = scanner_start_time if start_time.second > 0: start_time = start_time.replace(second=0, microsecond=0) print( f"--> back-testing {symbol} from {str(start_time)} duration {duration}" ) if debug_symbols and symbol in debug_symbols: print("--> using DEBUG mode") re_try = 3 while re_try > 0: # load historical data try: symbol_data = data_api.polygon.historic_agg_v2( symbol, 1, "minute", _from=str(start_time - timedelta(days=8)), to=str(start_time + timedelta(days=1)), limit=10000, ).df except HTTPError as e: tlog(f"Received HTTP error {e} for {symbol}") return if len(symbol_data) < 100: tlog(f"not enough data-points for {symbol}") return add_daily_vwap( symbol_data, debug=debug_symbols and symbol in debug_symbols, ) market_data.minute_history[symbol] = symbol_data print( f"loaded {len(market_data.minute_history[symbol].index)} agg data points" ) position: int = 0 try: minute_index = symbol_data["close"].index.get_loc( start_time, method="nearest" ) break except (Exception, ValueError) as e: print(f"[EXCEPTION] {e} - trying to reload-data. ") re_try -= 1 new_now = symbol_data.index[minute_index] print(f"start time with data {new_now}") price = 0.0 last_run_id = None # start_time + duration while ( new_now < config.market_close and minute_index < symbol_data.index.size - 1 ): if symbol_data.index[minute_index] != new_now: print( "mismatch!", symbol_data.index[minute_index], new_now ) print( symbol_data["close"][ minute_index - 10 : minute_index + 1 ] ) raise Exception() price = symbol_data["close"][minute_index] for strategy in trading_data.strategies: if debug_symbols and symbol in debug_symbols: print( f"Execute strategy {strategy.name} on {symbol} at {new_now}" ) do, what = await strategy.run( symbol, True, position, symbol_data[: minute_index + 1], new_now, portfolio_value, debug=debug_symbols and symbol in debug_symbols, # type: ignore backtesting=True, ) if do: if ( what["side"] == "buy" and float(what["qty"]) > 0 or what["side"] == "sell" and float(what["qty"]) < 0 ): position += int(float(what["qty"])) trading_data.buy_time[symbol] = new_now.replace( second=0, microsecond=0 ) else: position -= int(float(what["qty"])) trading_data.last_used_strategy[symbol] = strategy db_trade = NewTrade( algo_run_id=strategy.algo_run.run_id, symbol=symbol, qty=int(float(what["qty"])), operation=what["side"], price=price, indicators=trading_data.buy_indicators[symbol] if what["side"] == "buy" else trading_data.sell_indicators[symbol], ) await db_trade.save( config.db_conn_pool, str(new_now), trading_data.stop_prices[symbol], trading_data.target_prices[symbol], ) if what["side"] == "buy": await strategy.buy_callback( symbol, price, int(float(what["qty"])) ) break elif what["side"] == "sell": await strategy.sell_callback( symbol, price, int(float(what["qty"])) ) break last_run_id = strategy.algo_run.run_id minute_index += 1 new_now = symbol_data.index[minute_index] if position: if ( trading_data.last_used_strategy[symbol].type == StrategyType.DAY_TRADE ): tlog( f"[{new_now}]{symbol} liquidate {position} at {price}" ) db_trade = NewTrade( algo_run_id=last_run_id, # type: ignore symbol=symbol, qty=int(position) if int(position) > 0 else -int(position), operation="sell" if position > 0 else "buy", price=price, indicators={"liquidate": 1}, ) await db_trade.save( config.db_conn_pool, str(symbol_data.index[minute_index - 1]), )
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 backtest_symbol( data_loader: DataLoader, portfolio_value: float, symbol: str, start: datetime, duration: timedelta, scanner_start_time: datetime, debug_symbol: bool = False, ) -> None: est = pytz.timezone("America/New_York") scanner_start_time = (pytz.utc.localize(scanner_start_time).astimezone(est) if scanner_start_time.tzinfo is None else scanner_start_time) start_time = pytz.utc.localize(start).astimezone(est) if scanner_start_time > start_time + duration: print( f"{symbol} picked too late at {scanner_start_time} ({start_time}, {duration})" ) return start_time = scanner_start_time if start_time.second > 0: start_time = start_time.replace(second=0, microsecond=0) print( f"--> back-testing {symbol} from {str(start_time)} duration {duration}" ) if debug_symbol: print("--> using DEBUG mode") symbol_data = pd.DataFrame(data_loader[symbol][start_time:start_time + duration] # type: ignore ) add_daily_vwap( symbol_data, debug=debug_symbol, ) print( f"loaded {len(symbol_data)} agg data points({start_time}-{start_time + duration})" ) minute_index = symbol_data["close"].index.get_loc(start_time, method="nearest") position: int = 0 new_now = symbol_data.index[minute_index] print(f"start time with data {new_now}") price = 0.0 last_run_id = None # start_time + duration rejected: Dict[str, List] = {} while (new_now < config.market_close and minute_index < symbol_data.index.size - 1): if symbol_data.index[minute_index] != new_now: print("mismatch!", symbol_data.index[minute_index], new_now) print(symbol_data["close"][minute_index - 10:minute_index + 1]) raise Exception() price = symbol_data["close"][minute_index] for strategy in trading_data.strategies: if debug_symbol: print( f"Execute strategy {strategy.name} on {symbol} at {new_now}" ) if symbol in rejected.get(strategy.name, []): continue try: do, what = await strategy.run( symbol, True, position, symbol_data[:minute_index + 1], new_now, portfolio_value, debug=debug_symbol, # type: ignore backtesting=True, ) except Exception as e: traceback.print_exc() tlog( f"[ERROR] exception {e} on symbol {symbol} @ {strategy.name}" ) continue if do: if (what["side"] == "buy" and float(what["qty"]) > 0 or what["side"] == "sell" and float(what["qty"]) < 0): position += int(float(what["qty"])) trading_data.buy_time[symbol] = new_now.replace( second=0, microsecond=0) else: position -= int(float(what["qty"])) trading_data.last_used_strategy[symbol] = strategy db_trade = NewTrade( algo_run_id=strategy.algo_run.run_id, symbol=symbol, qty=int(float(what["qty"])), operation=what["side"], price=price, indicators=trading_data.buy_indicators[symbol] if what["side"] == "buy" else trading_data.sell_indicators[symbol], ) await db_trade.save( config.db_conn_pool, str(new_now), trading_data.stop_prices[symbol], trading_data.target_prices[symbol], ) if what["side"] == "buy": await strategy.buy_callback(symbol, price, int(float(what["qty"]))) break elif what["side"] == "sell": await strategy.sell_callback(symbol, price, int(float(what["qty"]))) break elif what.get("reject", False): if strategy.name in rejected: rejected[strategy.name].append(symbol) else: rejected[strategy.name] = [symbol] last_run_id = strategy.algo_run.run_id minute_index += 1 new_now = symbol_data.index[minute_index] if position: if (trading_data.last_used_strategy[symbol].type == StrategyType.DAY_TRADE): tlog(f"[{new_now}]{symbol} liquidate {position} at {price}") db_trade = NewTrade( algo_run_id=last_run_id, # type: ignore symbol=symbol, qty=int(position) if int(position) > 0 else -int(position), operation="sell" if position > 0 else "buy", price=price, indicators={"liquidate": 1}, ) await db_trade.save( config.db_conn_pool, str(symbol_data.index[minute_index - 1]), )
async def update_vwap(self, symbol: str, now: datetime) -> None: if not self.data_loader[symbol][now].average: add_daily_vwap(self.data_loader[symbol])
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, {}
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, {}
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, {}