def test_avg_of_last_elems(self): close = [1.0, 2.0, 3.0, 4.0] open = [2.0, 3.0, 4.0, 5.0] ohlcs = [ OHLC(date(2021, 1, 1) + timedelta(days=i), open=open[i], low=0.0, high=10.0, close=close[i], num_trades=1, volume=1.0, waprice=1.0) for i in range(len(close)) ] series = OHLCSeries("i", ohlcs) assert series.mean_of_last_elems(2) == (close[-2] + close[-1]) / 2 assert series.mean_of_last_elems( 2, lambda ohlc: ohlc.open) == (open[-2] + open[-1]) / 2 assert series.mean_of_last_elems(4) == (close[0] + close[1] + close[2] + close[3]) / 4 with pytest.raises(ValueError): series.mean_of_last_elems(5)
def get_triggered_signals(self, instr: moex.Instrument, ser: instruments.OHLCSeries, intraday_state: IntradayState) -> (Outcome, str): """returns tuple whose first element is true if triggered and second is message with details""" # TODO: for bonds: add notification its price gets < 100. try: instr.update_ohlc_table(ser) quote = instr.load_intraday_quotes() if not quote.is_trading: return Outcome.NOT_TRIGGERED_DUE_TO_NOT_READY, f"Skipping {instr} as it's not currently trading" time_of_last_trade = quote.time if intraday_state.time_of_last_trade is not None and \ time_of_last_trade < intraday_state.time_of_last_trade: # trading day switched logger.info( f"Trading day switched for {instr}, resetting intraday averager" ) intraday_state.moving_avg = MovingAvgCalculator( self.intraday_window_size) intraday_state.time_of_last_trade = time_of_last_trade # TODO: add only if time of last trade differs, to track only deals, not ticks intraday_state.moving_avg.add(quote.last) hist_mean = ser.mean_of_last_elems(self.hist_window_size) std_dev = ser.std_dev_of_last_elems(self.hist_window_size, mean=hist_mean) intra_mean = intraday_state.moving_avg.avg() if intra_mean is None: return Outcome.NOT_TRIGGERED_DUE_TO_NOT_READY, f"Skipping {instr} as it hasn't accumulated " \ f"{self.intraday_window_size} intraday quotes yet" rel_diff = (intra_mean - hist_mean) / max(intra_mean, hist_mean) nstd_devs = self.num_std_devs_thresh * std_dev observed_num_std_devs_jump = abs(intra_mean - hist_mean) / std_dev if intra_mean > hist_mean + nstd_devs: return Outcome.TRIGGERED, f"Jump UP {round(rel_diff * 100.0, 2)}% to {round(intra_mean, 2)} " \ f"({self.intraday_window_size} tick avg) " \ f"from average of {round(hist_mean, 2)} of last {self.hist_window_size} days " \ f"(jump of {round(observed_num_std_devs_jump, 2)} std. devs from mean)" elif intra_mean < hist_mean - nstd_devs: return Outcome.TRIGGERED, f"Jump DOWN {round(rel_diff * 100.0, 2)}% to {round(intra_mean, 2)} " \ f"({self.intraday_window_size} tick avg) " \ f"from average of {round(hist_mean, 2)} of last {self.hist_window_size} days " \ f"(jump of {round(observed_num_std_devs_jump, 2)} std. devs from mean)" else: return Outcome.NOT_TRIGGERED_DUE_TO_THRESHOLD, f"{round(intra_mean, 2)} ({self.intraday_window_size} tick avg) is " \ f"{round(rel_diff * 100.0, 2)}% jump over last " \ f"{self.hist_window_size} days average of {round(hist_mean, 2)} " \ f"(jump of {round(observed_num_std_devs_jump, 2)} std. devs from mean)" except Exception as ex: logger.error(f"Error happened checking {instr}", exc_info=ex, stack_info=True) return True, f"Could not check: {ex}"