Exemplo n.º 1
0
    def test_can_load_full_ohlc_from_partials(self):
        instr = m.FXInstrument("i")
        ohlc1 = OHLC(date(2005, 6, 20),
                     open=10.0,
                     low=5.0,
                     high=15.0,
                     close=12.0,
                     num_trades=1,
                     volume=100.0,
                     waprice=11.0)
        ohlc2 = OHLC(date(2005, 6, 23),
                     open=10.0,
                     low=5.0,
                     high=15.0,
                     close=13.0,
                     num_trades=2,
                     volume=100.0,
                     waprice=11.0)
        partial_replies = [
            OHLCSeries(instr.code, [ohlc1]),
            OHLCSeries(instr.code, [ohlc2]),
            OHLCSeries(instr.code, [])
        ]
        mocked_loader = MagicMock(side_effect=partial_replies)

        full_table = instr.load_ohlc_table(None, mocked_loader)
        assert not full_table.is_empty()
        assert full_table.ohlc_series == [ohlc1, ohlc2]
Exemplo n.º 2
0
 def update_ohlc_table(self, existing_series: OHLCSeries) -> None:
     """Tries to load new values after the last date of existing series"""
     if self.code != existing_series.instr_code:
         raise ValueError(
             f"Incompatible instruments: {self.code} vs {existing_series.instr_code}"
         )
     if not existing_series.is_empty():
         from_date = existing_series.ohlc_series[-1].date + one_day
     else:
         from_date = None
     addition = self.load_ohlc_table(from_date)
     existing_series.append(addition)
Exemplo n.º 3
0
    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}"
Exemplo n.º 4
0
 def save_series(self, instr: moex.Instrument, ser: instruments.OHLCSeries,
                 intraday_state: IntradayState, now: datetime.datetime):
     if intraday_state.time_last_save is None or now - intraday_state.time_last_save > self.saving_freq:
         logger.info(f"Saving series for {instr}")
         try:
             fname = instrument_to_filename(instr)
             ser.save_to_csv(fname)
             logger.info("Done saving")
             intraday_state.time_last_save = now
         except Exception as ex:
             logger.error(f"Failed to save series for {instr}",
                          exc_info=ex,
                          stack_info=True)
Exemplo n.º 5
0
 def _parse_ohlc_csv(self, reply: str) -> OHLCSeries:
     lines = reply.split("\n")[2:]
     # note field names in moex reply are OLHC (parsed here), but our native csv
     # (in instruments module) is OHLC as market convention
     reader = csv.DictReader(lines, delimiter=";")
     series = []
     name = None
     for row in reader:
         try:
             num_trades = self.parse_num_trades(row)
             date = datetime.date.fromisoformat(row["TRADEDATE"])
             if num_trades != 0:
                 ohlc = OHLC(date=date,
                             open=float(row["OPEN"]),
                             low=float(row["LOW"]),
                             high=float(row["HIGH"]),
                             close=float(row["CLOSE"]),
                             num_trades=num_trades,
                             volume=self.parse_volume(row),
                             waprice=self.parse_waprice(row))
                 series.append(ohlc)
             else:
                 # otherwise pandas import will change column type from double to object due to NA presence
                 logger.info(
                     f"Skipping {date} for {self} as it had no trades")
             if name is None:
                 name = row["SHORTNAME"]
         except ValueError as e:
             raise ValueError(f"Error happened for row {row}", e)
     if self.name is None:
         self.name = name
     return OHLCSeries(self.code, series, name)
Exemplo n.º 6
0
 def test_can_create_good_instance(self):
     series = OHLCSeries("instr", [
         OHLC(date(2020, 12, 4),
              open=9.0,
              low=9.0,
              high=15.0,
              close=11.0,
              num_trades=1,
              volume=10.0,
              waprice=13.0),
         OHLC(date(2020, 12, 5),
              open=9.0,
              low=9.0,
              high=15.0,
              close=11.0,
              num_trades=1,
              volume=10.0,
              waprice=13.0),
     ])
     assert not series.is_empty()
Exemplo n.º 7
0
 def test_can_grow_series(self):
     series1 = OHLCSeries("instr", [
         OHLC(date(2020, 12, 4),
              open=9.0,
              low=9.0,
              high=15.0,
              close=11.0,
              num_trades=1,
              volume=10.0,
              waprice=13.0),
         OHLC(date(2020, 12, 5),
              open=9.0,
              low=9.0,
              high=15.0,
              close=11.0,
              num_trades=1,
              volume=10.0,
              waprice=13.0),
     ])
     series2 = OHLCSeries("instr", [
         OHLC(date(2020, 12, 6),
              open=9.0,
              low=9.0,
              high=15.0,
              close=11.0,
              num_trades=1,
              volume=10.0,
              waprice=13.0),
         OHLC(date(2020, 12, 7),
              open=9.0,
              low=9.0,
              high=15.0,
              close=11.0,
              num_trades=1,
              volume=10.0,
              waprice=13.0),
     ])
     series1.append(series2)
     assert len(series1.ohlc_series) == 4
     assert series1.ohlc_series[2:] == series2.ohlc_series
Exemplo n.º 8
0
 def test_cannot_append_non_adjacent_series(self):
     with pytest.raises(ValueError) as e:
         series1 = OHLCSeries("instr", [
             OHLC(date(2020, 12, 4),
                  open=9.0,
                  low=9.0,
                  high=15.0,
                  close=11.0,
                  num_trades=1,
                  volume=10.0,
                  waprice=13.0),
             OHLC(date(2020, 12, 5),
                  open=9.0,
                  low=9.0,
                  high=15.0,
                  close=11.0,
                  num_trades=1,
                  volume=10.0,
                  waprice=13.0),
         ])
         series2 = OHLCSeries("instr", [
             OHLC(date(2020, 12, 5),
                  open=9.0,
                  low=9.0,
                  high=15.0,
                  close=11.0,
                  num_trades=1,
                  volume=10.0,
                  waprice=13.0),
             OHLC(date(2020, 12, 6),
                  open=9.0,
                  low=9.0,
                  high=15.0,
                  close=11.0,
                  num_trades=1,
                  volume=10.0,
                  waprice=13.0),
         ])
         series1.append(series2)
     assert "First date (2020-12-05) must be > 2020-12-05" in str(e.value)
Exemplo n.º 9
0
 def load_ohlc_table(
     self,
     from_date: Optional[datetime.date] = None,
     partial_loader: Callable[[Instrument, Optional[datetime.date]],
                              OHLCSeries] = __load_partial_ohlc_table_csv
 ) -> OHLCSeries:
     """loads OHLC table from web API of exchange, starting from the specified date or from beginning if empty.
     Note for some instruments MOEX API can give data starting from later date than available on their site"""
     series = OHLCSeries(self.code, [], None)
     date = from_date
     while True:
         logger.info(
             f"Loading {self} from {date if date is not None else 'beginning'}"
         )
         addition = partial_loader(self, date)
         if addition.is_empty():
             break
         else:
             series.append(addition)
             date = addition.ohlc_series[-1].date + datetime.timedelta(
                 days=1)
     return series
Exemplo n.º 10
0
 def test_cannot_create_with_dates_not_ascending(self):
     with pytest.raises(ValueError):
         OHLCSeries("instr", [
             OHLC(date(2020, 12, 5),
                  open=9.0,
                  low=9.0,
                  high=15.0,
                  close=11.0,
                  num_trades=1,
                  volume=10.0,
                  waprice=13.0),
             OHLC(date(2020, 12, 4),
                  open=9.0,
                  low=9.0,
                  high=15.0,
                  close=11.0,
                  num_trades=1,
                  volume=10.0,
                  waprice=13.0)
         ])
Exemplo n.º 11
0
 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)
Exemplo n.º 12
0
    def test_empty_processing(self):
        s1 = OHLCSeries("i", [])
        s1.append(OHLCSeries("i", []))
        assert s1.is_empty()

        s_tmp = OHLCSeries("i", [
            OHLC(date(2020, 12, 6),
                 open=9.0,
                 low=9.0,
                 high=15.0,
                 close=11.0,
                 num_trades=1,
                 volume=10.0,
                 waprice=13.0)
        ])
        s2 = OHLCSeries("i", [])
        s2.append(s_tmp)
        assert not s2.is_empty()
        assert s2 == s_tmp

        s_tmp.append(OHLCSeries("i", []))
        assert not s_tmp.is_empty()
Exemplo n.º 13
0
 def test_cannot_join_series_of_different_instruments(self):
     with pytest.raises(ValueError) as e:
         series1 = OHLCSeries("instr1", [])
         series2 = OHLCSeries("instr2", [])
         series1.append(series2)
     assert "Instruments do not match" in str(e.value)
Exemplo n.º 14
0
 def test_can_create_empty_series(self):
     series = OHLCSeries("instr", [])
     assert series.is_empty()