diff --git a/pdr_backend/aimodel/aimodel_data_factory.py b/pdr_backend/aimodel/aimodel_data_factory.py index 2e0b3fa7a..d369f08e3 100644 --- a/pdr_backend/aimodel/aimodel_data_factory.py +++ b/pdr_backend/aimodel/aimodel_data_factory.py @@ -1,6 +1,6 @@ import logging import sys -from typing import Optional, Tuple +from typing import List, Optional, Tuple import numpy as np import pandas as pd @@ -68,8 +68,8 @@ def create_xy( self, mergedohlcv_df: pl.DataFrame, testshift: int, - feed: ArgFeed, - feeds: Optional[ArgFeeds] = None, + predict_feed: ArgFeed, + train_feeds: Optional[ArgFeeds] = None, do_fill_nans: bool = True, ) -> Tuple[np.ndarray, np.ndarray, pd.DataFrame, np.ndarray]: """ @@ -80,6 +80,8 @@ def create_xy( @arguments mergedohlcv_df -- *polars* DataFrame. See class docstring testshift -- to simulate across historical test data + predict_feed -- feed to predict + train_feeds -- feeds to use for model inputs. If None use predict feed do_fill_nans -- if any values are nan, fill them? (Via interpolation) If you turn this off and mergedohlcv_df has nans, then X/y/etc gets nans @@ -94,27 +96,30 @@ def create_xy( assert "timestamp" in mergedohlcv_df.columns assert "datetime" not in mergedohlcv_df.columns - # every column should be ordered with oldest first, youngest last. - # let's verify! The timestamps should be in ascending order + # condition mergedohlcv_df + # - every column should be ordered with oldest first, youngest last. + # let's verify! The timestamps should be in ascending order uts = mergedohlcv_df["timestamp"].to_list() assert uts == sorted(uts, reverse=False) - - # condition inputs if do_fill_nans and has_nan(mergedohlcv_df): mergedohlcv_df = fill_nans(mergedohlcv_df) - ss = self.ss.aimodel_ss - x_dim_len = 0 - if not feeds: - x_dim_len = ss.n - feeds = ss.feeds + + # condition other inputs + train_feeds_list: List[ArgFeed] + if train_feeds: + train_feeds_list = train_feeds else: - x_dim_len = len(feeds) * ss.autoregressive_n + train_feeds_list = [predict_feed] + ss = self.ss.aimodel_ss + x_dim_len = len(train_feeds_list) * ss.autoregressive_n + # main work x_df = pd.DataFrame() # build this up xrecent_df = pd.DataFrame() # "" target_hist_cols = [ - f"{feed.exchange}:{feed.pair}:{feed.signal}" for feed in feeds + f"{train_feed.exchange}:{train_feed.pair}:{train_feed.signal}" + for train_feed in train_feeds_list ] for hist_col in target_hist_cols: assert hist_col in mergedohlcv_df.columns, f"missing data col: {hist_col}" @@ -146,7 +151,7 @@ def create_xy( # y is set from yval_{exch_str, signal_str, pair_str} # eg y = [BinEthC_-1, BinEthC_-2, ..., BinEthC_-450, BinEthC_-451] - hist_col = f"{feed.exchange}:{feed.pair}:{feed.signal}" + hist_col = f"{predict_feed.exchange}:{predict_feed.pair}:{predict_feed.signal}" z = mergedohlcv_df[hist_col].to_list() y = np.array(_slice(z, -testshift - N_train - 1, -testshift)) diff --git a/pdr_backend/aimodel/test/test_aimodel_data_factory.py b/pdr_backend/aimodel/test/test_aimodel_data_factory.py index 6bfaca332..d9313b89e 100644 --- a/pdr_backend/aimodel/test/test_aimodel_data_factory.py +++ b/pdr_backend/aimodel/test/test_aimodel_data_factory.py @@ -1,12 +1,10 @@ +from enforce_typing import enforce_types +import numpy as np +from numpy.testing import assert_array_equal import pandas as pd import polars as pl import pytest -import numpy as np - -from numpy.testing import assert_array_equal -from enforce_typing import enforce_types -from pdr_backend.cli.predict_feeds import PredictFeeds from pdr_backend.aimodel.aimodel_data_factory import AimodelDataFactory from pdr_backend.lake.merge_df import merge_rawohlcv_dfs from pdr_backend.lake.test.resources import ( @@ -35,17 +33,24 @@ def test_ycont_to_ytrue(): @enforce_types def test_create_xy__0(): - predict_feeds = [ - {"predict": "binanceus ETH/USDT c 5m", "train_on": "binanceus ETH/USDT c 5m"} + # create predictoor_ss + feedset_list = [ + { + "predict": "binanceus ETH/USDT c 5m", + "train_on": "binanceus ETH/USDT oc 5m", + } ] - d = predictoor_ss_test_dict( - predict_feeds=predict_feeds, - input_feeds=["binanceus ETH/USDT oc"], - ) + d = predictoor_ss_test_dict(feedset_list=feedset_list) + assert "aimodel_ss" in d + assert "max_n_train" in d["aimodel_ss"] d["aimodel_ss"]["max_n_train"] = 4 + + assert "autoregressive_n" in d["aimodel_ss"] d["aimodel_ss"]["autoregressive_n"] = 2 + predictoor_ss = PredictoorSS(d) + # create df mergedohlcv_df = pl.DataFrame( { # every column is ordered from youngest to oldest @@ -56,6 +61,7 @@ def test_create_xy__0(): } ) + # set target X,y target_X = np.array( [ [0.1, 0.1, 3.1, 4.2], # oldest @@ -75,12 +81,21 @@ def test_create_xy__0(): ) target_xrecent = np.array([0.1, 0.1, 8.6, 9.7]) - factory = AimodelDataFactory(predictoor_ss) - target_y = np.array([5.3, 6.4, 7.5, 8.6, 9.7]) # oldest to newest + + # do work + testshift = 0 + factory = AimodelDataFactory(predictoor_ss) + predict_feed = predictoor_ss.predict_train_feedsets[0].predict + train_feeds = predictoor_ss.predict_train_feedsets[0].train_on X, y, x_df, xrecent = factory.create_xy( - mergedohlcv_df, testshift=0, feed=predictoor_ss.feeds.feeds[0] + mergedohlcv_df, + testshift, + predict_feed, + train_feeds, ) + + # test result _assert_pd_df_shape(predictoor_ss.aimodel_ss, X, y, x_df) assert_array_equal(X, target_X) assert_array_equal(y, target_y) @@ -90,11 +105,16 @@ def test_create_xy__0(): @enforce_types def test_create_xy_reg__1exchange_1coin_1signal(): - predict_feeds = [ - {"predict": "binanceus ETH/USDT h 5m", "train_on": "binanceus ETH/USDT h 5m"} + feedset_list = [ + { + "predict": "binanceus ETH/USDT h 5m", + "train_on": "binanceus ETH/USDT h 5m", + } ] - d = predictoor_ss_test_dict(predict_feeds) + d = predictoor_ss_test_dict(feedset_list=feedset_list) predictoor_ss = PredictoorSS(d) + predict_feed = predictoor_ss.predict_train_feedsets[0].predict + train_feeds = predictoor_ss.predict_train_feedsets[0].train_on aimodel_data_factory = AimodelDataFactory(predictoor_ss) mergedohlcv_df = merge_rawohlcv_dfs(ETHUSDT_RAWOHLCV_DFS) @@ -133,8 +153,12 @@ def test_create_xy_reg__1exchange_1coin_1signal(): ) target_xrecent = np.array([3.0, 2.0, 1.0]) + testshift = 0 X, y, x_df, xrecent = aimodel_data_factory.create_xy( - mergedohlcv_df, testshift=0, feed=predictoor_ss.feeds.feeds[0] + mergedohlcv_df, + testshift, + predict_feed, + train_feeds, ) _assert_pd_df_shape(predictoor_ss.aimodel_ss, X, y, x_df) @@ -177,8 +201,12 @@ def test_create_xy_reg__1exchange_1coin_1signal(): ) target_xrecent = np.array([4.0, 3.0, 2.0]) + testshift = 1 X, y, x_df, xrecent = aimodel_data_factory.create_xy( - mergedohlcv_df, testshift=1, feed=predictoor_ss.feeds.feeds[0] + mergedohlcv_df, + testshift, + predict_feed, + train_feeds, ) _assert_pd_df_shape(predictoor_ss.aimodel_ss, X, y, x_df) @@ -210,8 +238,12 @@ def test_create_xy_reg__1exchange_1coin_1signal(): assert "max_n_train" in predictoor_ss.aimodel_ss.d predictoor_ss.aimodel_ss.d["max_n_train"] = 5 + testshift = 0 X, y, x_df, _ = aimodel_data_factory.create_xy( - mergedohlcv_df, testshift=0, feed=predictoor_ss.feeds.feeds[0] + mergedohlcv_df, + testshift, + predict_feed, + train_feeds, ) _assert_pd_df_shape(predictoor_ss.aimodel_ss, X, y, x_df) @@ -222,6 +254,21 @@ def test_create_xy_reg__1exchange_1coin_1signal(): @enforce_types def test_create_xy_reg__2exchanges_2coins_2signals(): + # create predictoor_ss + feedset_list = [ + { + "predict": "binanceus ETH/USDT h 5m", + "train_on": [ + "binanceus BTC/USDT ETH/USDT hl 5m", + "kraken BTC/USDT ETH/USDT hl 5m", + ], + } + ] + d = predictoor_ss_test_dict(feedset_list=feedset_list) + ss = PredictoorSS(d) + assert ss.aimodel_ss.autoregressive_n == 3 + + # create mergedohlcv_df rawohlcv_dfs = { "binanceus": { "BTC/USDT": _df_from_raw_data(BINANCE_BTC_DATA), @@ -232,34 +279,22 @@ def test_create_xy_reg__2exchanges_2coins_2signals(): "ETH/USDT": _df_from_raw_data(KRAKEN_ETH_DATA), }, } - - d = predictoor_ss_test_dict() - assert "feeds" in d - assert "input_feeds" in d["aimodel_ss"] - d["predict_feed"] = PredictFeeds.from_array( - [ - { - "predict": "binanceus ETH/USDT h 5m", - "train_on": "binanceus ETH/USDT h 5m", - } - ] - ) - d["aimodel_ss"]["input_feeds"] = [ - "binanceus BTC/USDT,ETH/USDT hl", - "kraken BTC/USDT,ETH/USDT hl", - ] - ss = PredictoorSS(d) - - assert ss.aimodel_ss.autoregressive_n == 3 - assert ss.aimodel_ss.n == (4 + 4) * 3 - mergedohlcv_df = merge_rawohlcv_dfs(rawohlcv_dfs) + # create X, y, x_df aimodel_data_factory = AimodelDataFactory(ss) + testshift = 0 + predict_feed = ss.predict_train_feedsets[0].predict + train_feeds = ss.predict_train_feedsets[0].train_on + assert len(train_feeds) == 8 X, y, x_df, _ = aimodel_data_factory.create_xy( - mergedohlcv_df, testshift=0, feed=d["predict_feed"].feeds[0] + mergedohlcv_df, + testshift, + predict_feed, + train_feeds, ) + # test X, y, x_df _assert_pd_df_shape(ss.aimodel_ss, X, y, x_df) found_cols = x_df.columns.tolist() target_cols = [ @@ -290,7 +325,7 @@ def test_create_xy_reg__2exchanges_2coins_2signals(): ] assert found_cols == target_cols - # test binanceus:ETH/USDT:high like in 1-signal + # - test binanceus:ETH/USDT:high like in 1-signal assert target_cols[3:6] == [ "binanceus:ETH/USDT:high:t-4", "binanceus:ETH/USDT:high:t-3", @@ -305,17 +340,9 @@ def test_create_xy_reg__2exchanges_2coins_2signals(): assert x_df.iloc[-2].tolist()[3:6] == [5, 4, 3] assert x_df.iloc[0].tolist()[3:6] == [11, 10, 9] - assert x_df["binanceus:ETH/USDT:high:t-2"].tolist() == [ - 9, - 8, - 7, - 6, - 5, - 4, - 3, - 2, - ] - assert Xa[:, 2].tolist() == [9, 8, 7, 6, 5, 4, 3, 2] + target_list = [9, 8, 7, 6, 5, 4, 3, 2] + assert x_df["binanceus:ETH/USDT:high:t-2"].tolist() == target_list + assert Xa[:, 2].tolist() == target_list @enforce_types @@ -327,43 +354,49 @@ def test_create_xy_reg__check_timestamp_order(): assert uts == sorted(uts, reverse=False) # happy path - feed = factory.ss.feeds[0] - factory.create_xy(mergedohlcv_df, testshift=0, feed=feed.predict) + testshift = 0 + predict_feed = factory.ss.predict_train_feedsets[0].predict + factory.create_xy(mergedohlcv_df, testshift, predict_feed) # failure path bad_uts = sorted(uts, reverse=True) # bad order bad_mergedohlcv_df = mergedohlcv_df.with_columns(pl.Series("timestamp", bad_uts)) with pytest.raises(AssertionError): - factory.create_xy(bad_mergedohlcv_df, testshift=0, feed=feed.predict) + factory.create_xy(bad_mergedohlcv_df, testshift, predict_feed) @enforce_types def test_create_xy_reg__input_type(): - mergedohlcv_df, aimodel_data_factory = _mergedohlcv_df_ETHUSDT() + mergedohlcv_df, factory = _mergedohlcv_df_ETHUSDT() assert isinstance(mergedohlcv_df, pl.DataFrame) - assert isinstance(aimodel_data_factory, AimodelDataFactory) + assert isinstance(factory, AimodelDataFactory) # create_xy() input should be pl - feed = aimodel_data_factory.ss.feeds[0] - aimodel_data_factory.create_xy(mergedohlcv_df, testshift=0, feed=feed.predict) + testshift = 0 + predict_feed = factory.ss.predict_train_feedsets[0].predict + factory.create_xy(mergedohlcv_df, testshift, predict_feed) # create_xy() inputs shouldn't be pd + pandas_df = mergedohlcv_df.to_pandas() with pytest.raises(AssertionError): - aimodel_data_factory.create_xy( - mergedohlcv_df.to_pandas(), testshift=0, feed=feed.predict - ) + factory.create_xy(pandas_df, testshift, predict_feed) @enforce_types def test_create_xy_reg__handle_nan(): # create mergedohlcv_df - predict_feeds = [ - {"predict": "binanceus ETH/USDT h 5m", "train_on": "binanceus ETH/USDT h 5m"} + feeds = [ + { + "predict": "binanceus ETH/USDT h 5m", + "train_on": "binanceus ETH/USDT h 5m", + } ] - d = predictoor_ss_test_dict(predict_feeds) + d = predictoor_ss_test_dict(feedset_list=feeds) predictoor_ss = PredictoorSS(d) - aimodel_data_factory = AimodelDataFactory(predictoor_ss) + predict_feed = predictoor_ss.predict_train_feedsets[0].predict + testshift = 0 + factory = AimodelDataFactory(predictoor_ss) mergedohlcv_df = merge_rawohlcv_dfs(ETHUSDT_RAWOHLCV_DFS) # initial mergedohlcv_df should be ok @@ -381,14 +414,13 @@ def test_create_xy_reg__handle_nan(): ) assert has_nan(mergedohlcv_df) - # =========== initial testshift (0) # run create_xy() and force the nans to stick around # -> we want to ensure that we're building X/y with risk of nan - X, y, x_df, _ = aimodel_data_factory.create_xy( + X, y, x_df, _ = factory.create_xy( mergedohlcv_df, - testshift=0, + testshift, + predict_feed, do_fill_nans=False, - feed=predictoor_ss.feeds.feeds[0], ) assert has_nan(X) and has_nan(y) and has_nan(x_df) @@ -397,17 +429,19 @@ def test_create_xy_reg__handle_nan(): assert not has_nan(mergedohlcv_df2) # nan approach 2: explicitly tell create_xy to fill nans - X, y, x_df, _ = aimodel_data_factory.create_xy( + X, y, x_df, _ = factory.create_xy( mergedohlcv_df, - testshift=0, + testshift, + predict_feed, do_fill_nans=True, - feed=predictoor_ss.feeds.feeds[0], ) assert not has_nan(X) and not has_nan(y) and not has_nan(x_df) # nan approach 3: create_xy fills nans by default (best) - X, y, x_df, _ = aimodel_data_factory.create_xy( - mergedohlcv_df, testshift=0, feed=predictoor_ss.feeds.feeds[0] + X, y, x_df, _ = factory.create_xy( + mergedohlcv_df, + testshift, + predict_feed, ) assert not has_nan(X) and not has_nan(y) and not has_nan(x_df) @@ -422,7 +456,5 @@ def _assert_pd_df_shape( ): assert X.shape[0] == y.shape[0] assert X.shape[0] == (ss.max_n_train + 1) # 1 for test, rest for train - assert X.shape[1] == ss.n assert len(x_df) == X.shape[0] - assert len(x_df.columns) == ss.n diff --git a/pdr_backend/cli/arg_feed.py b/pdr_backend/cli/arg_feed.py index 0576d5366..ffd398e1b 100644 --- a/pdr_backend/cli/arg_feed.py +++ b/pdr_backend/cli/arg_feed.py @@ -35,10 +35,10 @@ def __init__( if timeframe is None: self.timeframe = None + elif isinstance(timeframe, str): + self.timeframe = ArgTimeframe(timeframe) else: - self.timeframe = ( - ArgTimeframe(timeframe) if isinstance(timeframe, str) else timeframe - ) + self.timeframe = timeframe def __str__(self): feed_str = f"{self.exchange} {self.pair}" @@ -52,7 +52,8 @@ def __str__(self): return feed_str - def __eq__(self, other): + def __eq__(self, other) -> bool: + assert isinstance(other, ArgFeed), (other, type(other)) return ( self.exchange == other.exchange and str(self.signal) == str(other.signal) diff --git a/pdr_backend/cli/arg_pair.py b/pdr_backend/cli/arg_pair.py index dc5de0351..502a2ff01 100644 --- a/pdr_backend/cli/arg_pair.py +++ b/pdr_backend/cli/arg_pair.py @@ -39,9 +39,9 @@ def __init__( verify_base_str(base_str) verify_quote_str(quote_str) - self.pair_str = pair_str - self.base_str = base_str - self.quote_str = quote_str + self.pair_str: str = pair_str # type: ignore[assignment] + self.base_str: str = base_str # type: ignore[assignment] + self.quote_str: str = quote_str # type: ignore[assignment] def __eq__(self, other): return self.pair_str == str(other) diff --git a/pdr_backend/cli/cli_module.py b/pdr_backend/cli/cli_module.py index bfbd18a2f..d85840bbb 100644 --- a/pdr_backend/cli/cli_module.py +++ b/pdr_backend/cli/cli_module.py @@ -73,12 +73,10 @@ def do_sim(args, nested_args=None): network="development", nested_override_args=nested_args, ) - # TO-DO Find a way to pass a specific feed - # Or update simengine to simulate on all feeds - feed = ppss.predictoor_ss.feeds[0] - if len(ppss.predictoor_ss.feeds) > 0: - logger.warning("Multiple prediction feeds provided, using the first one") - sim_engine = SimEngine(ppss, feed) + feedset = ppss.predictoor_ss.predict_train_feedsets[0] + if len(ppss.predictoor_ss.predict_train_feedsets) > 0: + logger.warning("Multiple predict feeds provided, using the first one") + sim_engine = SimEngine(ppss, feedset) sim_engine.run() diff --git a/pdr_backend/cli/parse_feed_obj.py b/pdr_backend/cli/parse_feed_obj.py new file mode 100644 index 000000000..75fec11a6 --- /dev/null +++ b/pdr_backend/cli/parse_feed_obj.py @@ -0,0 +1,28 @@ +from typing import List, Union + +from enforce_typing import enforce_types + +from pdr_backend.cli.arg_feed import ArgFeed +from pdr_backend.cli.arg_feeds import ArgFeeds + + +@enforce_types +def parse_feed_obj(feed_obj: Union[str, list]) -> ArgFeeds: + # Create feed_list + if isinstance(feed_obj, list): + feed_list = feed_obj + elif isinstance(feed_obj, str): + if "," in feed_obj: + feed_list = feed_obj.split(",") + else: + feed_list = [feed_obj] + else: + raise ValueError(feed_obj) + + # Parse feed_list + parsed_arg_feeds: ArgFeeds = ArgFeeds([]) + for feed in feed_list: + arg_feeds: List[ArgFeed] = ArgFeeds.from_str(str(feed)) + parsed_arg_feeds.extend(arg_feeds) + + return parsed_arg_feeds diff --git a/pdr_backend/cli/predict_feeds.py b/pdr_backend/cli/predict_feeds.py deleted file mode 100644 index 25c88d58a..000000000 --- a/pdr_backend/cli/predict_feeds.py +++ /dev/null @@ -1,121 +0,0 @@ -from typing import List, Optional, Union -from enforce_typing import enforce_types -from pdr_backend.cli.arg_feed import ArgFeed -from pdr_backend.cli.arg_feeds import ArgFeeds -from pdr_backend.cli.arg_pair import ArgPair - - -@enforce_types -def parse_feed_obj(feed_obj: Union[str, list]) -> ArgFeeds: - # If feed_obj is a string, convert to list - if isinstance(feed_obj, str): - # If comma separated string, split - # If not comma separated, convert to list - if "," in feed_obj: - feed_obj = feed_obj.split(",") - else: - feed_obj = [feed_obj] - - if not isinstance(feed_obj, list): - raise ValueError(f"feed_obj must be a list, got {feed_obj}") - - parsed_objs: ArgFeeds = ArgFeeds([]) - for feed in feed_obj: - # Convert each feed_obj string to ArgFeeds - arg_feeds: List[ArgFeed] = ArgFeeds.from_str(str(feed)) - parsed_objs.extend(arg_feeds) - return parsed_objs - - -@enforce_types -class PredictFeed: - def __init__(self, predict: ArgFeed, train_on: ArgFeeds): - self.predict: ArgFeed = predict - self.train_on: ArgFeeds = train_on - - @classmethod - def from_feed_objs(cls, predict: ArgFeed, train_on: Union[str, list]): - parsed_train_on: ArgFeeds = parse_feed_obj(train_on) - return cls(predict, parsed_train_on) - - def to_dict(self): - return {"predict": self.predict, "train_on": self.train_on} - - @classmethod - def from_dict(cls, d): - return cls(d["predict"], d["train_on"]) - - @property - def timeframe_ms(self) -> int: - """Returns timeframe, in ms""" - return self.predict.timeframe.ms if self.predict.timeframe else 0 - - @property - def predict_quote_str(self) -> Optional[str]: - return ArgPair(self.predict.pair).quote_str - - @property - def predict_base_str(self) -> Optional[str]: - return ArgPair(self.predict.pair).base_str - - @property - def predict_pair_str(self) -> Optional[str]: - return ArgPair(self.predict.pair).pair_str - - -@enforce_types -class PredictFeeds(List[PredictFeed]): - def __init__(self, feeds: List[PredictFeed]): - super().__init__(feeds) - - @classmethod - def from_array(cls, feeds: List[dict]): - fin = [] - for pairs in feeds: - predict = pairs.get("predict") - train_on = pairs.get("train_on") - if train_on is None: - raise ValueError(f"train_on must be provided, got {pairs}") - if predict is None: - raise ValueError(f"predict must be provided, got {pairs}") - predict = parse_feed_obj(predict) - for p in predict: - fin.append(PredictFeed.from_feed_objs(p, train_on)) - return cls(fin) - - @property - def feeds_str(self) -> List[str]: - set_pairs = [] - for feed in self: - for pairs in [feed.train_on]: - for pair in pairs: - if str(pair) not in set_pairs: - set_pairs.append(str(pair)) - if str(feed.predict) not in set_pairs: - set_pairs.append(str(feed.predict)) - return set_pairs - - @property - def feeds(self) -> List[ArgFeed]: - set_pairs = [] - for feed in self: - for pairs in [feed.train_on]: - for pair in pairs: - if pair not in set_pairs: - set_pairs.append(pair) - if feed.predict not in set_pairs: - set_pairs.append(feed.predict) - return set_pairs - - @property - def min_epoch_seconds(self) -> int: - epoch = 1e9 - for feed in self: - assert ( - feed.predict.timeframe is not None - ), f"Feed: {feed} is is missing timeframe" - epoch = min(epoch, feed.predict.timeframe.s) - return int(epoch) - - def to_list(self) -> List[dict]: - return [feed.to_dict() for feed in self] diff --git a/pdr_backend/cli/predict_train_feedset.py b/pdr_backend/cli/predict_train_feedset.py new file mode 100644 index 000000000..0c8f14ed6 --- /dev/null +++ b/pdr_backend/cli/predict_train_feedset.py @@ -0,0 +1,69 @@ +from typing import List + +from enforce_typing import enforce_types +from typeguard import check_type + +from pdr_backend.cli.arg_feed import ArgFeed +from pdr_backend.cli.arg_feeds import ArgFeeds + + +class PredictTrainFeedset: + """ + Easy manipulation of a single set of (predict feed / train_on feeds). + + To be precise, it has: + - 1 feed to predict + - >=1 feeds as inputs to the model + """ + + @enforce_types + def __init__(self, predict: ArgFeed, train_on: ArgFeeds): + self.predict: ArgFeed = predict + self.train_on: ArgFeeds = train_on + + @enforce_types + def __str__(self) -> str: + return str(self.to_dict()) + + @enforce_types + def __eq__(self, other): + return self.predict == other.predict and self.train_on == other.train_on + + @enforce_types + def to_dict(self): + return {"predict": str(self.predict), "train_on": str(self.train_on)} + + @classmethod + def from_dict(cls, feedset_dict: dict) -> "PredictTrainFeedset": + """ + @arguments + feedset_dict -- has the following format: + {"predict":predict_feed_str (1 feed), + "train_on":train_on_feeds_str (>=1 feeds)} + Note just ONE predict feed is allowed, not >=1. + + Here are three examples. from_dict() gives the same output for each. + 1. { "predict" : "binance BTC/USDT o 1h", + "train_on" : "binance BTC/USDT ETH/USDT o 1h"} + 2. { "predict" : "binance BTC/USDT o 1h", + "train_on" : "binance BTC/USDT o 1h, binance ETH/USDT o 1h"} + 3. { "predict" : "binance BTC/USDT o 1h", + "train_on" : ["binance BTC/USDT o 1h", "binance ETH/USDT o 1h"]} + """ + predict = ArgFeed.from_str(feedset_dict["predict"]) + train_on = ArgFeeds.from_strs(_as_list(feedset_dict["train_on"])) + return cls(predict, train_on) + + @property + def timeframe_ms(self) -> int: + """Returns timeframe, in ms""" + return self.predict.timeframe.ms if self.predict.timeframe else 0 + + +@enforce_types +def _as_list(str_or_list) -> List[str]: + """Given a str or a list of str, always returns as a list""" + if isinstance(str_or_list, str): + return [str_or_list] + check_type(str_or_list, List[str]) # raises TypeCheckError if wrong type + return str_or_list diff --git a/pdr_backend/cli/predict_train_feedsets.py b/pdr_backend/cli/predict_train_feedsets.py new file mode 100644 index 000000000..78e82e4fe --- /dev/null +++ b/pdr_backend/cli/predict_train_feedsets.py @@ -0,0 +1,123 @@ +from typing import List + +from enforce_typing import enforce_types + +from pdr_backend.cli.arg_feed import ArgFeed +from pdr_backend.cli.arg_feeds import ArgFeeds +from pdr_backend.cli.parse_feed_obj import parse_feed_obj +from pdr_backend.cli.predict_train_feedset import PredictTrainFeedset + + +class PredictTrainFeedsets(List[PredictTrainFeedset]): + """ + Easy manipulation of all (predict/train_on) PredictTrainFeedset objects. + Includes a way to parse from the raw yaml inputs. + """ + + @enforce_types + def __init__(self, feedsets: List[PredictTrainFeedset]): + super().__init__(feedsets) + + @enforce_types + def __str__(self) -> str: + strs = [str(feedset) for feedset in self] + return "[" + ", ".join(strs) + "]" + + @classmethod + @enforce_types + def from_list_of_dict(cls, feedset_list: List[dict]) -> "PredictTrainFeedsets": + """ + @arguments + feedset_list -- list of feedset_dict, + where feedset_dict has the following format: + {"predict":predict_feeds_str, + "train_on":train_on_feeds_str} + Note that >=1 predict feeds are allowed for a given feedset_dict. + + Example feedset_list = [ + { + "predict": "binance BTC/USDT c 5m, kraken BTC/USDT c 5m", + "train_on": [ + "binance BTC/USDT ETH/USDT DOT/USDT c 5m", + "kraken BTC/USDT c 5m", + ], + }, + { + "predict": "binance ETH/USDT ADA/USDT c 5m", + "train_on": "binance BTC/USDT DOT/USDT c 5m, kraken BTC/USDT c 5m", + }, + """ + final_list = [] + for feedset_dict in feedset_list: + if not ("predict" in feedset_dict and "train_on" in feedset_dict): + raise ValueError(feedset_dict) + + predict_feeds: ArgFeeds = parse_feed_obj(feedset_dict["predict"]) + for predict in predict_feeds: + train_on = parse_feed_obj(feedset_dict["train_on"]) + feedset = PredictTrainFeedset(predict, train_on) + final_list.append(feedset) + + return cls(final_list) + + @enforce_types + def to_list_of_dict(self) -> List[dict]: + """Like from_list_of_dict(), but in reverse""" + return [feedset.to_dict() for feedset in self] + + @property + def feed_strs(self) -> List[str]: + """ + Return eg ['binance BTC/USDT DOT/USDT c 5m','kraken BTC/USDT c 5m'] + """ + return [str(feed) for feed in self.feeds] + + @property + def feeds(self) -> List[ArgFeed]: + feed_list = [] + for feedset in self: + for train_feeds in [feedset.train_on]: + for train_feed in train_feeds: + if train_feed not in feed_list: + feed_list.append(train_feed) + + predict_feed = feedset.predict + if predict_feed not in feed_list: + feed_list.append(predict_feed) + + return feed_list + + @property + def min_epoch_seconds(self) -> int: + epoch = 1e9 + for feedset in self: + predict_feed = feedset.predict + assert predict_feed.timeframe is not None, predict_feed + + epoch = min(epoch, predict_feed.timeframe.s) + return int(epoch) + + @enforce_types + def get_feedset( + self, + exchange_str: str, + pair_str: str, + timeframe_str: str, + ): + """ + @description + Return a feedset if the input (exchange, pair, timeframe) + is found in one of the predict feeds + + Example input: "binance", "BTC/USDT", "5m" + """ + for feedset in self: + predict_feed: ArgFeed = feedset.predict + if ( + str(predict_feed.exchange) == exchange_str + and str(predict_feed.pair) == pair_str + and str(predict_feed.timeframe) == timeframe_str + ): + return feedset + + return None diff --git a/pdr_backend/cli/test/test_parse_feed_obj.py b/pdr_backend/cli/test/test_parse_feed_obj.py new file mode 100644 index 000000000..d5f78f14f --- /dev/null +++ b/pdr_backend/cli/test/test_parse_feed_obj.py @@ -0,0 +1,21 @@ +from enforce_typing import enforce_types + +from pdr_backend.cli.arg_feeds import ArgFeeds +from pdr_backend.cli.parse_feed_obj import parse_feed_obj + + +@enforce_types +def test_parse_feed_obj(): + feed_str = "binance BTC/USDT ETH/USDT o 1h, kraken ADA/USDT c 5m" + + parsed = parse_feed_obj(feed_str) + + assert type(parsed) == ArgFeeds + assert str(parsed) == "binance BTC/USDT ETH/USDT o 1h, kraken ADA/USDT c 5m" + + feed_list = ["binance BTC/USDT ETH/USDT o 1h", "kraken ADA/USDT c 5m"] + + parsed = parse_feed_obj(feed_list) + + assert type(parsed) == ArgFeeds + assert str(parsed) == "binance BTC/USDT ETH/USDT o 1h, kraken ADA/USDT c 5m" diff --git a/pdr_backend/cli/test/test_predict_feed.py b/pdr_backend/cli/test/test_predict_feed.py deleted file mode 100644 index 00627ab00..000000000 --- a/pdr_backend/cli/test/test_predict_feed.py +++ /dev/null @@ -1,73 +0,0 @@ -# pylint: disable=redefined-outer-name -import pytest - -from pdr_backend.cli.arg_feed import ArgFeed -from pdr_backend.cli.arg_feeds import ArgFeeds -from pdr_backend.cli.predict_feeds import PredictFeed, parse_feed_obj - - -@pytest.fixture -def arg_feed_single(): - return ArgFeed("binance", "open", "BTC/USDT", "1h") - - -@pytest.fixture -def arg_feed_list() -> ArgFeeds: - return ArgFeeds.from_str("binance BTC/USDT ETH/USDT o 1h") - - -def test_predict_feed_initialization(arg_feed_single, arg_feed_list): - predict_feed = PredictFeed(predict=arg_feed_single, train_on=arg_feed_list) - assert predict_feed.predict == arg_feed_single - assert predict_feed.train_on == arg_feed_list - - -def test_predict_feed_from_feed_objs(arg_feed_single, arg_feed_list): - predict_feed = PredictFeed.from_feed_objs( - predict=arg_feed_single, train_on=arg_feed_list - ) - assert predict_feed.predict == arg_feed_single - assert predict_feed.train_on == arg_feed_list - - -def test_predict_feed_from_dict(arg_feed_single, arg_feed_list): - dict_feed = {"predict": arg_feed_single, "train_on": arg_feed_list} - predict_feed = PredictFeed.from_dict(dict_feed) - assert predict_feed.predict == arg_feed_single - assert predict_feed.train_on == arg_feed_list - - -def test_predict_feed_timeframe_ms(arg_feed_single, arg_feed_list): - predict_feed = PredictFeed(predict=arg_feed_single, train_on=arg_feed_list) - assert predict_feed.timeframe_ms == arg_feed_single.timeframe.ms - - -def test_predict_feed_predict_quote_str(arg_feed_single, arg_feed_list): - predict_feed = PredictFeed(predict=arg_feed_single, train_on=arg_feed_list) - assert predict_feed.predict_quote_str == "USDT" - - -def test_predict_feed_predict_base_str(arg_feed_single, arg_feed_list): - predict_feed = PredictFeed(predict=arg_feed_single, train_on=arg_feed_list) - assert predict_feed.predict_base_str == "BTC" - - -def test_predict_feed_predict_pair_str(arg_feed_single, arg_feed_list): - predict_feed = PredictFeed(predict=arg_feed_single, train_on=arg_feed_list) - assert predict_feed.predict_pair_str == "BTC/USDT" - - -def test_parse_feed_obj(): - feed_str = "binance BTC/USDT ETH/USDT o 1h, kraken ADA/USDT c 5m" - - parsed = parse_feed_obj(feed_str) - - assert type(parsed) == ArgFeeds - assert str(parsed) == "binance BTC/USDT ETH/USDT o 1h, kraken ADA/USDT c 5m" - - feed_list = ["binance BTC/USDT ETH/USDT o 1h", "kraken ADA/USDT c 5m"] - - parsed = parse_feed_obj(feed_list) - - assert type(parsed) == ArgFeeds - assert str(parsed) == "binance BTC/USDT ETH/USDT o 1h, kraken ADA/USDT c 5m" diff --git a/pdr_backend/cli/test/test_predict_feeds.py b/pdr_backend/cli/test/test_predict_feeds.py deleted file mode 100644 index fe7857f35..000000000 --- a/pdr_backend/cli/test/test_predict_feeds.py +++ /dev/null @@ -1,74 +0,0 @@ -# pylint: disable=redefined-outer-name -import pytest -from pdr_backend.cli.arg_feed import ArgFeed -from pdr_backend.cli.arg_feeds import ArgFeeds -from pdr_backend.cli.predict_feeds import PredictFeed, PredictFeeds - - -@pytest.fixture -def arg_feed_single(): - return ArgFeed("binance", "open", "BTC/USDT", "1h") - - -@pytest.fixture -def arg_feed_list() -> ArgFeeds: - return ArgFeeds.from_str("binance BTC/USDT ETH/USDT o 1h") - - -@pytest.fixture -def arg_feed_predict_str(): - return "binance BTC/USDT o 1h" - - -@pytest.fixture -def arg_feed_train_on_str(): - return "binance BTC/USDT ETH/USDT o 1h" - - -@pytest.fixture -def predict_feed_fixture(arg_feed_single, arg_feed_list): - return PredictFeed(predict=arg_feed_single, train_on=arg_feed_list) - - -def test_predict_feeds_initialization(predict_feed_fixture): - feeds = [predict_feed_fixture] - predict_feeds = PredictFeeds(feeds) - assert len(predict_feeds) == 1 - assert predict_feeds[0] == predict_feed_fixture - - -def test_predict_feeds_from_array_valid_data( - arg_feed_train_on_str, arg_feed_predict_str -): - array = [{"predict": arg_feed_predict_str, "train_on": arg_feed_train_on_str}] - predict_feeds = PredictFeeds.from_array(array) - assert len(predict_feeds) == 1 - assert predict_feeds[0].predict == ArgFeed.from_str(arg_feed_predict_str) - assert predict_feeds[0].train_on == ArgFeeds.from_str(arg_feed_train_on_str) - assert predict_feeds.min_epoch_seconds == 3600 - - -def test_predict_feeds_from_array_missing_predict(arg_feed_train_on_str): - with pytest.raises(ValueError) as excinfo: - PredictFeeds.from_array([{"train_on": arg_feed_train_on_str}]) - assert "predict must be provided" in str(excinfo.value) - - -def test_predict_feeds_from_array_missing_train_on(arg_feed_predict_str): - with pytest.raises(ValueError) as excinfo: - PredictFeeds.from_array([{"predict": arg_feed_predict_str}]) - assert "train_on must be provided" in str(excinfo.value) - - -def test_predict_feeds_properties(predict_feed_fixture): - predict_feeds = PredictFeeds([predict_feed_fixture, predict_feed_fixture]) - assert len(predict_feeds.feeds_str) == 2 - assert len(predict_feeds.feeds) == 2 - assert predict_feeds.min_epoch_seconds == 3600 - - -def test_predict_feeds_to_list(predict_feed_fixture): - predict_feeds = PredictFeeds([predict_feed_fixture]) - result = predict_feeds.to_list() - assert isinstance(result, list) - assert result[0]["predict"] == predict_feed_fixture.predict diff --git a/pdr_backend/cli/test/test_predict_train_feedset.py b/pdr_backend/cli/test/test_predict_train_feedset.py new file mode 100644 index 000000000..bc0d7b3ca --- /dev/null +++ b/pdr_backend/cli/test/test_predict_train_feedset.py @@ -0,0 +1,81 @@ +from enforce_typing import enforce_types +import pytest +from typeguard import TypeCheckError + +from pdr_backend.cli.arg_feed import ArgFeed +from pdr_backend.cli.arg_feeds import ArgFeeds +from pdr_backend.cli.predict_train_feedsets import PredictTrainFeedset + + +# for "predict" +ARG_FEED_STR = "binance BTC/USDT o 1h" +ARG_FEED: ArgFeed = ArgFeed.from_str(ARG_FEED_STR) + +# for "train_on" +ARG_FEEDS_STR = "binance BTC/USDT ETH/USDT o 1h" +ARG_FEEDS: ArgFeeds = ArgFeeds.from_str(ARG_FEEDS_STR) + + +@enforce_types +def test_feedset_main(): + feedset = PredictTrainFeedset(predict=ARG_FEED, train_on=ARG_FEEDS) + + assert feedset.predict == ARG_FEED + assert feedset.train_on == ARG_FEEDS + assert feedset.timeframe_ms == ARG_FEED.timeframe.ms + + assert feedset.to_dict() == {"predict": ARG_FEED_STR, "train_on": ARG_FEEDS_STR} + + assert ( + str(feedset) + == "{'predict': 'binance BTC/USDT o 1h', 'train_on': 'binance BTC/USDT ETH/USDT o 1h'}" + ) + + +@enforce_types +def test_feedset_eq_same(): + feedset1 = PredictTrainFeedset(predict=ARG_FEED, train_on=ARG_FEEDS) + feedset2 = PredictTrainFeedset(predict=ARG_FEED, train_on=ARG_FEEDS) + assert feedset1 == feedset2 + + +@enforce_types +def test_feedset_eq_diff(): + feedset1 = PredictTrainFeedset(predict=ARG_FEED, train_on=ARG_FEEDS) + + arg_feed2 = ArgFeed.from_str("kraken BTC/USDT o 1h") + arg_feeds2 = ArgFeeds.from_str("kraken BTC/USDT ETH/USDT o 1h") + + # different "predict" + feedset2a = PredictTrainFeedset(predict=arg_feed2, train_on=ARG_FEEDS) + assert feedset1 != feedset2a + + # different "train_on" + feedset2b = PredictTrainFeedset(predict=ARG_FEED, train_on=arg_feeds2) + assert feedset1 != feedset2b + + +@enforce_types +def test_feedset_from_dict(): + # "train_on" as str + d = {"predict": ARG_FEED_STR, "train_on": ARG_FEEDS_STR} + feedset = PredictTrainFeedset.from_dict(d) + assert feedset.predict == ARG_FEED + assert feedset.train_on == ARG_FEEDS + assert feedset.to_dict() == d + + # "train_on" as list + d = {"predict": ARG_FEED_STR, "train_on": [ARG_FEEDS_STR]} + feedset = PredictTrainFeedset.from_dict(d) + assert feedset.predict == ARG_FEED + assert feedset.train_on == ARG_FEEDS + + # "predict" value must be a str + d = {"predict": ARG_FEED, "train_on": ARG_FEEDS_STR} + with pytest.raises(TypeError): + feedset = PredictTrainFeedset.from_dict(d) + + # "train_on" value must be a str + d = {"predict": ARG_FEED_STR, "train_on": ARG_FEEDS} + with pytest.raises(TypeCheckError): + feedset = PredictTrainFeedset.from_dict(d) diff --git a/pdr_backend/cli/test/test_predict_train_feedsets.py b/pdr_backend/cli/test/test_predict_train_feedsets.py new file mode 100644 index 000000000..9a3dcfa47 --- /dev/null +++ b/pdr_backend/cli/test/test_predict_train_feedsets.py @@ -0,0 +1,150 @@ +from enforce_typing import enforce_types +import pytest + +from pdr_backend.cli.arg_feed import ArgFeed +from pdr_backend.cli.arg_feeds import ArgFeeds +from pdr_backend.cli.predict_train_feedsets import ( + PredictTrainFeedset, + PredictTrainFeedsets, +) + +# for "predict" +ARG_FEED_STR = "binance BTC/USDT o 1h" +ARG_FEED: ArgFeed = ArgFeed.from_str(ARG_FEED_STR) + +# for "train_on" +ARG_FEEDS_STR = "binance BTC/USDT ETH/USDT o 1h" +ARG_FEEDS: ArgFeeds = ArgFeeds.from_str(ARG_FEEDS_STR) + +# ("predict", "train_on") set +FEEDSET_DICT = {"predict": ARG_FEED_STR, "train_on": ARG_FEEDS_STR} +FEEDSET = PredictTrainFeedset(predict=ARG_FEED, train_on=ARG_FEEDS) + + +@enforce_types +def test_feedsets_1_feedset(): + feedsets = PredictTrainFeedsets([FEEDSET]) + assert len(feedsets) == 1 + assert feedsets[0] == FEEDSET + assert feedsets.to_list_of_dict() == [FEEDSET_DICT] + assert feedsets.feed_strs == ["binance BTC/USDT o 1h", "binance ETH/USDT o 1h"] + assert feedsets.feeds == [ARG_FEEDS[0], ARG_FEEDS[1]] + assert feedsets.min_epoch_seconds == 3600 + + assert feedsets == PredictTrainFeedsets([FEEDSET]) + assert ( + str(feedsets) + == "[{'predict': 'binance BTC/USDT o 1h', 'train_on': 'binance BTC/USDT ETH/USDT o 1h'}]" + ) + + feedsets2 = PredictTrainFeedsets.from_list_of_dict([FEEDSET_DICT]) + assert feedsets2 == feedsets + + +@enforce_types +def test_feedsets_2_feedsets(): + feedsets = PredictTrainFeedsets([FEEDSET, FEEDSET]) + assert len(feedsets) == 2 + assert feedsets[0] == feedsets[1] == FEEDSET + assert feedsets.to_list_of_dict() == [FEEDSET_DICT, FEEDSET_DICT] + assert feedsets.feed_strs == ["binance BTC/USDT o 1h", "binance ETH/USDT o 1h"] + assert feedsets.feeds == [ARG_FEEDS[0], ARG_FEEDS[1]] + assert feedsets.min_epoch_seconds == 3600 + + assert feedsets == PredictTrainFeedsets([FEEDSET, FEEDSET]) + + feedsets2 = PredictTrainFeedsets.from_list_of_dict([FEEDSET_DICT, FEEDSET_DICT]) + assert feedsets2 == feedsets + + +@enforce_types +def test_feedsets_from_list_of_dict__bad_paths(): + # bad path: missing "predict" field + with pytest.raises(ValueError): + PredictTrainFeedsets.from_list_of_dict([{"train_on": ARG_FEEDS_STR}]) + + # bad path: missing "train_on" field + with pytest.raises(ValueError): + PredictTrainFeedsets.from_list_of_dict([{"predict": ARG_FEED_STR}]) + + +@enforce_types +def test_feedsets_from_list_of_dict__thorough(): + feedset_list = [ + { + "predict": "binance BTC/USDT c 5m, kraken BTC/USDT c 5m", + "train_on": [ + "binance BTC/USDT ETH/USDT DOT/USDT c 5m", + "kraken BTC/USDT c 5m", + ], + }, + { + "predict": "binance ETH/USDT ADA/USDT c 5m", + "train_on": "binance BTC/USDT DOT/USDT c 5m, kraken BTC/USDT c 5m", + }, + ] + + feedsets = PredictTrainFeedsets.from_list_of_dict(feedset_list) + + # - why 4 and not 2? Because each "predict" entry had _two_ feeds + # - therefore 2 feeds for first dict, and 2 feeds for second dict + # - 2 + 2 = 4 :) + assert len(feedsets) == 4 + + feedset_list2 = feedsets.to_list_of_dict() + + # when it outputs a list of dict, it doesn't compact back to 2 dicts. OK! + target_feedset_list2 = [ + { + "predict": "binance BTC/USDT c 5m", + "train_on": "binance BTC/USDT DOT/USDT ETH/USDT c 5m, kraken BTC/USDT c 5m", + }, + { + "predict": "kraken BTC/USDT c 5m", + "train_on": "binance BTC/USDT DOT/USDT ETH/USDT c 5m, kraken BTC/USDT c 5m", + }, + { + "predict": "binance ETH/USDT c 5m", + "train_on": "binance BTC/USDT DOT/USDT c 5m, kraken BTC/USDT c 5m", + }, + { + "predict": "binance ADA/USDT c 5m", + "train_on": "binance BTC/USDT DOT/USDT c 5m, kraken BTC/USDT c 5m", + }, + ] + assert feedset_list2 == target_feedset_list2 + + +@enforce_types +def test_feedsets__get_feedset(): + feedset_list = [ + { + "predict": "binance BTC/USDT c 5m", + "train_on": "binance BTC/USDT c 5m", + }, + { + "predict": "kraken BTC/USDT ETH/USDT c 5m", + "train_on": "kraken BTC/USDT ETH/USDT DOT/USDT c 5m", + }, + ] + feedsets = PredictTrainFeedsets.from_list_of_dict(feedset_list) + f0, f1, f2 = feedsets[0], feedsets[1], feedsets[2] + + assert f0.predict == ArgFeed("binance", "close", "BTC/USDT", "5m") + assert f1.predict == ArgFeed("kraken", "close", "BTC/USDT", "5m") + assert f2.predict == ArgFeed("kraken", "close", "ETH/USDT", "5m") + + assert feedsets.get_feedset("binance", "BTC/USDT", "5m") == f0 + assert feedsets.get_feedset("kraken", "BTC/USDT", "5m") == f1 + assert feedsets.get_feedset("kraken", "ETH/USDT", "5m") == f2 + + assert feedsets.get_feedset("foo", "BTC/USDT", "5m") is None + assert feedsets.get_feedset("binance", "foo", "5m") is None + assert feedsets.get_feedset("binance", "BTC/USDT", "foo") is None + + with pytest.raises(TypeError): + feedsets.get_feedset(1, "BTC/USDT", "5m") + with pytest.raises(TypeError): + feedsets.get_feedset("binance", 1, "5m") + with pytest.raises(TypeError): + feedsets.get_feedset("binance", "BTC/USDT", 1) diff --git a/pdr_backend/contract/pred_submitter_mgr.py b/pdr_backend/contract/pred_submitter_mgr.py index 396ee8347..313a39e4d 100644 --- a/pdr_backend/contract/pred_submitter_mgr.py +++ b/pdr_backend/contract/pred_submitter_mgr.py @@ -1,57 +1,51 @@ from typing import List + from enforce_typing import enforce_types + from pdr_backend.contract.base_contract import BaseContract +from pdr_backend.ppss.web3_pp import Web3PP from pdr_backend.util.currency_types import Wei from pdr_backend.util.time_types import UnixTimeS @enforce_types class PredSubmitterMgr(BaseContract): - def __init__(self, web3_pp, address: str): + def __init__(self, web3_pp: Web3PP, address: str): """ - @description - Initialize the PredSubmitterMgr object with a web3 provider and contract address. - @arguments - web3_pp -- Web3 provider for interacting with the blockchain. - address -- str, the address of the PredSubmitterMgr contract. + web3_pp -- Web3 provider for interacting with the blockchain + address -- address of PredSubmitterMgr contract """ super().__init__(web3_pp, address, "PredSubmitterMgr") def predictoor_up_address(self) -> str: """ - @description - Returns the address of the upward predictoor contract. - @return - address -- str, address of the upward predictoor contract. + address -- str, address of upward predictoor contract """ return self.contract_instance.functions.instanceUp().call() def predictoor_down_address(self) -> str: """ - @description - Returns the address of the downward predictoor contract. - @return - address -- str, address of the downward predictoor contract. + address -- address of downward predictoor contract """ return self.contract_instance.functions.instanceDown().call() def claim_dfrewards( - self, token_addr: str, dfrewards_addr: str, wait_for_receipt=True + self, token_addr: str, dfrewards_addr: str, wait_for_receipt: bool = True ): """ @description - Claims DF rewards for the given token from the DFRewards contract. + Claims DF rewards for the given token from the DFRewards contract @arguments - token_addr -- str, address of the token contract. - dfrewards_addr -- str, address of the DFRewards contract. - wait_for_receipt -- bool, if True, waits for the transaction receipt. + token_addr -- address of token contract + dfrewards_addr -- address of DFRewards contract + wait_for_receipt -- if True, waits for the tx receipt @return - tx -- transaction hash if wait_for_receipt is False, else the transaction receipt. + tx -- tx hash if wait_for_receipt is False, else the tx receipt """ call_params = self.web3_pp.tx_call_params() tx = self.contract_instance.functions.claimDFRewards( @@ -67,55 +61,61 @@ def submit_prediction( self, stakes_up: List[Wei], stakes_down: List[Wei], - feeds: list, + feed_addrs: List[str], epoch: UnixTimeS, - wait_for_receipt=True, + wait_for_receipt: bool = True, ): """ @description Submits predictions for both upward and downward instances. @arguments - stakes_up -- list of Wei, stakes for the upward predictions. - stakes_down -- list of Wei, stakes for the downward predictions. - feeds -- list of str, addresses of the feeds for predictions. - epoch -- int, epoch start time for the predictions. - wait_for_receipt -- bool, if True, waits for the transaction receipt. + stakes_up -- stakes for upward predictions + stakes_down -- stakes for downward predictions + feed_addrs -- addresses of tje feeds for predictions + epoch -- epoch start time for the predictions + wait_for_receipt -- if True, waits for the tx receipt @return - tx -- transaction hash if wait_for_receipt is False, else the transaction receipt. + tx -- tx hash if wait_for_receipt is False, else the tx receipt """ stakes_up_wei = [s.amt_wei for s in stakes_up] stakes_down_wei = [s.amt_wei for s in stakes_down] if self.config.is_sapphire: _, tx = self.send_encrypted_tx( - "submit", [stakes_up_wei, stakes_down_wei, feeds, epoch] + "submit", [stakes_up_wei, stakes_down_wei, feed_addrs, epoch] ) else: call_params = self.web3_pp.tx_call_params() tx = self.contract_instance.functions.submit( - stakes_up_wei, stakes_down_wei, feeds, epoch + stakes_up_wei, stakes_down_wei, feed_addrs, epoch ).transact(call_params) if not wait_for_receipt: return tx return self.config.w3.eth.wait_for_transaction_receipt(tx) - def get_payout(self, epochs: list, feeds: list, wait_for_receipt=True): + def get_payout( + self, + epochs: List[UnixTimeS], + feed_addrs: List[str], + wait_for_receipt: bool = True, + ): """ @description - Claims payouts for given list of epochs for both upward and downward predictions. + Claims payouts for given list of epochs, + for both upward and downward predictions. @arguments - epochs -- list of int, epoch timestamps for the predictions to claim payouts for. - feeds -- list of str, addresses of the feeds for which to claim payouts. - wait_for_receipt -- bool, if True, waits for the transaction receipt. + epochs -- epoch timestamps for the predictions to claim payouts for + feed_addrs -- addresses of the feeds for which to claim payouts + wait_for_receipt -- if True, waits for the tx receipt @return - tx -- transaction hash if wait_for_receipt is False, else the transaction receipt. + tx -- tx hash if wait_for_receipt is False, else the tx receipt. """ call_params = self.web3_pp.tx_call_params() - tx = self.contract_instance.functions.getPayout(epochs, feeds).transact( + tx = self.contract_instance.functions.getPayout(epochs, feed_addrs).transact( call_params ) @@ -124,23 +124,29 @@ def get_payout(self, epochs: list, feeds: list, wait_for_receipt=True): return self.config.w3.eth.wait_for_transaction_receipt(tx) - def transfer_erc20(self, token: str, to: str, amount: Wei, wait_for_receipt=True): + def transfer_erc20( + self, + token_addr: str, + to_addr: str, + amount: Wei, + wait_for_receipt: bool = True, + ): """ @description - Transfers any ERC20 token from this contract to another address. + Transfers any ERC20 token from this contract to another address @arguments - token -- str, address of the ERC20 token contract. - to -- str, address of the recipient. - amount -- int, amount of tokens to transfer. - wait_for_receipt -- bool, if True, waits for the transaction receipt. + token_addr -- address of the ERC20 token contract + to_addr -- address of the recipient + amount -- # tokens to transfer + wait_for_receipt -- if True, waits for the tx receipt @return - tx -- transaction hash if wait_for_receipt is False, else the transaction receipt. + tx -- tx hash if wait_for_receipt is False, else the tx receipt """ call_params = self.web3_pp.tx_call_params() tx = self.contract_instance.functions.transferERC20( - token, to, amount.amt_wei + token_addr, to_addr, amount.amt_wei ).transact(call_params) if not wait_for_receipt: @@ -150,44 +156,47 @@ def transfer_erc20(self, token: str, to: str, amount: Wei, wait_for_receipt=True def version(self) -> str: """ - @description - Returns the version of the PredSubmitterMgr contract. - @return - version -- str, version of the contract. + version -- version of the PredSubmitterMgr contract """ return self.contract_instance.functions.version().call() - def approve_ocean(self, feeds: list, wait_for_receipt=True): + def approve_ocean( + self, + feed_addrs: List[str], + wait_for_receipt: bool = True, + ): """ @description - Approves infinite OCEAN tokens from the instances to the feeds. + Approves infinite OCEAN tokens from the instances to the feeds @arguments - feeds -- list of str, addresses of the feeds to approve tokens for. - wait_for_receipt -- bool, if True, waits for the transaction receipt. + feed_addrs -- addresses of the feeds to approve tokens for + wait_for_receipt -- if True, waits for the tx receipt @return - tx -- transaction hash if wait_for_receipt is False, else the transaction receipt. + tx -- tx hash if wait_for_receipt is False, else the tx receipt """ call_params = self.web3_pp.tx_call_params() - tx = self.contract_instance.functions.approveOcean(feeds).transact(call_params) + tx = self.contract_instance.functions.approveOcean(feed_addrs).transact( + call_params + ) if not wait_for_receipt: return tx return self.config.w3.eth.wait_for_transaction_receipt(tx) - def transfer(self, wait_for_receipt=True): + def transfer(self, wait_for_receipt: bool = True): """ @description - Transfers native tokens from this contract to the owner. + Transfers native tokens from this contract to the owner @arguments - wait_for_receipt -- bool, if True, waits for the transaction receipt. + wait_for_receipt -- if True, waits for the tx receipt @return - tx -- transaction hash if wait_for_receipt is False, else the transaction receipt. + tx -- tx hash if wait_for_receipt is False, else the tx receipt """ call_params = self.web3_pp.tx_call_params() tx = self.contract_instance.functions.transfer().transact(call_params) diff --git a/pdr_backend/deployer/util/models/PredictoorAgentConfig.py b/pdr_backend/deployer/util/models/PredictoorAgentConfig.py index 6f445d0f2..f93e7c0d9 100644 --- a/pdr_backend/deployer/util/models/PredictoorAgentConfig.py +++ b/pdr_backend/deployer/util/models/PredictoorAgentConfig.py @@ -49,15 +49,17 @@ def get_run_command( ): lake_feed_name = full_pair_name.replace(" c", "") override_feed = [ - f"--predictoor_ss.predict_feed={full_pair_name}", - f'--predictoor_ss.aimodel_ss.input_feeds=["{full_pair_name}"]', f'--lake_ss.feeds=["{lake_feed_name}"]', + f"--predictoor_ss.predict_train_feedsets=" + f' ["predict":"{full_pair_name}",' + f' "train_on":["{full_pair_name}"]]', ] if with_apostrophe: override_feed = [ - f'--predictoor_ss.predict_feed="{full_pair_name}"', - f"--predictoor_ss.aimodel_ss.input_feeds='[\"{full_pair_name}\"]'", f"--lake_ss.feeds='[\"{lake_feed_name}\"]'", + f"--predictoor_ss.predict_train_feedsets='" + f" ['predict':\"{full_pair_name}\"," + f" 'train_on':'[\"{full_pair_name}\"]]'", ] override_stake = [f"--predictoor_ss.stake_amount={stake_amt}"] override_s_until = [ diff --git a/pdr_backend/deployer/util/models/test/test_predictoor_agent_config.py b/pdr_backend/deployer/util/models/test/test_predictoor_agent_config.py index da1a1724d..6763f2fae 100644 --- a/pdr_backend/deployer/util/models/test/test_predictoor_agent_config.py +++ b/pdr_backend/deployer/util/models/test/test_predictoor_agent_config.py @@ -1,6 +1,9 @@ +from enforce_typing import enforce_types + from pdr_backend.deployer.util.models.PredictoorAgentConfig import PredictoorAgentConfig +@enforce_types def test_predictoor_agent_config(): agent_config = PredictoorAgentConfig( private_key="0x1", diff --git a/pdr_backend/lake/test/resources.py b/pdr_backend/lake/test/resources.py index 2efa2f9e7..58a8fcf3f 100644 --- a/pdr_backend/lake/test/resources.py +++ b/pdr_backend/lake/test/resources.py @@ -18,13 +18,13 @@ @enforce_types def _mergedohlcv_df_ETHUSDT(): - predict_feeds = [ + predict_train_feedsets = [ { "predict": "binanceus ETH/USDT h 5m", "train_on": "binanceus ETH/USDT h 5m", } ] - d = predictoor_ss_test_dict(predict_feeds) + d = predictoor_ss_test_dict(predict_train_feedsets) predictoor_ss = PredictoorSS(d) aimodel_data_factory = AimodelDataFactory(predictoor_ss) mergedohlcv_df = merge_rawohlcv_dfs(ETHUSDT_RAWOHLCV_DFS) diff --git a/pdr_backend/ppss/aimodel_ss.py b/pdr_backend/ppss/aimodel_ss.py index 17aac0256..694a6fbda 100644 --- a/pdr_backend/ppss/aimodel_ss.py +++ b/pdr_backend/ppss/aimodel_ss.py @@ -3,7 +3,6 @@ import numpy as np from enforce_typing import enforce_types -from pdr_backend.ppss.base_ss import MultiFeedMixin from pdr_backend.util.strutil import StrMixin APPROACH_OPTIONS = ["LinearLogistic", "LinearSVC", "Constant"] @@ -12,15 +11,13 @@ CALIBRATE_PROBS_OPTIONS = ["CalibratedClassifierCV_5x", "None"] -@enforce_types -class AimodelSS(MultiFeedMixin, StrMixin): +class AimodelSS(StrMixin): __STR_OBJDIR__ = ["d"] - FEEDS_KEY = "input_feeds" + @enforce_types def __init__(self, d: dict): - super().__init__( - d, assert_feed_attributes=["signal"] - ) # yaml_dict["aimodel_ss"] + """d -- yaml_dict["aimodel_ss"]""" + self.d = d # test inputs if not 0 < self.max_n_train: @@ -70,15 +67,6 @@ def calibrate_probs(self) -> str: """eg 'CalibratedClassifierCV_5x'""" return self.d["calibrate_probs"] - # input feeds defined in base - - # -------------------------------- - # derivative properties - @property - def n(self) -> int: - """Number of input dimensions == # columns in X""" - return self.n_feeds * self.autoregressive_n - # ========================================================================= # utilities for testing @@ -86,7 +74,6 @@ def n(self) -> int: @enforce_types def aimodel_ss_test_dict( - input_feeds: Optional[list] = None, max_n_train: Optional[int] = None, autoregressive_n: Optional[int] = None, approach: Optional[str] = None, @@ -96,7 +83,6 @@ def aimodel_ss_test_dict( ) -> dict: """Use this function's return dict 'd' to construct AimodelSS(d)""" d = { - "input_feeds": input_feeds or ["binance BTC/USDT c"], "max_n_train": 7 if max_n_train is None else max_n_train, "autoregressive_n": 3 if autoregressive_n is None else autoregressive_n, "approach": approach or "LinearLogistic", diff --git a/pdr_backend/ppss/base_ss.py b/pdr_backend/ppss/base_ss.py index 65e85bf79..dc08f5db8 100644 --- a/pdr_backend/ppss/base_ss.py +++ b/pdr_backend/ppss/base_ss.py @@ -97,6 +97,7 @@ def filter_feeds_from_candidates( class SingleFeedMixin: FEED_KEY = "" + @enforce_types def __init__(self, d: dict, assert_feed_attributes: Optional[List] = None): assert self.__class__.FEED_KEY self.d = d diff --git a/pdr_backend/ppss/ppss.py b/pdr_backend/ppss/ppss.py index beebb6113..af131d706 100644 --- a/pdr_backend/ppss/ppss.py +++ b/pdr_backend/ppss/ppss.py @@ -5,7 +5,7 @@ import yaml from enforce_typing import enforce_types -from pdr_backend.cli.predict_feeds import PredictFeeds +from pdr_backend.cli.predict_train_feedsets import PredictTrainFeedsets from pdr_backend.ppss.dfbuyer_ss import DFBuyerSS from pdr_backend.ppss.lake_ss import LakeSS from pdr_backend.ppss.multisim_ss import MultisimSS @@ -96,63 +96,58 @@ def constructor_dict( def verify_feed_dependencies(self): """Raise ValueError if a feed dependency is violated""" lake_fs = self.lake_ss.feeds - predict_fs = self.predictoor_ss.feeds - aimodel_fs = self.predictoor_ss.aimodel_ss.feeds + feedsets = self.predictoor_ss.predict_train_feedsets - # is predictoor_ss.predict_feed in lake feeds? + # basic tests + assert lake_fs + assert feedsets + + # does lake feeds hold each predict feed? Each train feed? # - check for matching {exchange, pair, timeframe} but not {signal} # because lake holds all signals o,h,l,c,v - for predict_f in predict_fs.feeds: + for feedset in feedsets: + predict_f, train_fs = feedset.predict, feedset.train_on + if not lake_fs.contains_combination( predict_f.exchange, predict_f.pair, predict_f.timeframe ): - s = "predictoor_ss.predict_feed not in lake_ss.feeds" - s += f"\n lake_ss.feeds = {lake_fs} (ohlcv)" - s += f"\n predictoor_ss.predict_feed = {predict_f}" - raise ValueError(s) - - # enforce that all predict feeds have the same timeframe - timeframe = "" - for predict_f in predict_fs.feeds: - if timeframe == "": - timeframe = predict_f.timeframe - continue - if predict_f.timeframe != timeframe: - s = "predictoor_ss.predict_feed not in lake_ss.feeds" + s = "a predict feed isn't in lake_ss.feeds" + s += f"\n predict feed = {predict_f}" s += f"\n lake_ss.feeds = {lake_fs} (ohlcv)" - s += f"\n predictoor_ss.predict_feed = {predict_f}" raise ValueError(s) - # do all aimodel_ss input feeds conform to predict feed timeframe? - for predict_f in predict_fs.feeds: - for aimodel_f in aimodel_fs: - if aimodel_f.timeframe != predict_f.timeframe: - s = "at least one ai_model_ss.input_feeds' timeframe incorrect" - s += f"\n target={predict_f.timeframe}, in predictoor_ss.feed" - s += f"\n found={aimodel_f.timeframe}, in this aimodel feed:" - s += f" {aimodel_f}" + for train_f in train_fs: + if not lake_fs.contains_combination( + predict_f.exchange, predict_f.pair, predict_f.timeframe + ): + s = "a training feed isn't in lake_ss.feeds" + s += f"\n training feed = {train_f}" + s += f"\n lake_ss.feeds = {lake_fs} (ohlcv)" raise ValueError(s) - # is each predictoor_ss.aimodel_ss.input_feeds in lake feeds? - # - check for matching {exchange, pair, timeframe} but not {signal} - for aimodel_f in aimodel_fs: - if not lake_fs.contains_combination( - aimodel_f.exchange, aimodel_f.pair, aimodel_f.timeframe + # do all feeds in predict/train sets have identical timeframe? + ok = True + ref_timeframe = feedsets[0].predict.timeframe + for feedset in feedsets: + predict_f, train_fs = feedset.predict, feedset.train_on + ok = ok and predict_f.timeframe == ref_timeframe + for train_f in feedset.train_on: + ok = ok and train_f.timeframe == ref_timeframe + + if not ok: + s = "predict/train feedsets have inconsistent timeframes" + s += f"\n predict_train_feedsets = {feedsets}" + raise ValueError(s) + + # for each feedset: is the predict feed in the corr. train feeds? + for feedset in feedsets: + predict_f, train_fs = feedset.predict, feedset.train_on + if not train_fs.contains_combination( + predict_f.exchange, predict_f.pair, predict_f.timeframe ): - s = "at least one aimodel_ss.input_feeds not in lake_ss.feeds" - s += f"\n lake_ss.feeds = {lake_fs} (ohlcv)" - s += f"\n predictoor_ss.ai_model.input_feeds = {aimodel_fs}" - s += f"\n (input_feed not found: {aimodel_f})" - raise ValueError(s) - - # is predictoor_ss.predict_feed in aimodel_ss.input_feeds? - # - check for matching {exchange, pair, timeframe AND signal} - for predict_f in predict_fs.feeds: - if predict_f not in aimodel_fs: - s = "predictoor_ss.predict_feed not in aimodel_ss.input_feeds" - s += " (accounting for signal too)" - s += f"\n predictoor_ss.ai_model.input_feeds = {aimodel_fs}" - s += f"\n predictoor_ss.predict_feed = {predict_f}" + s = "a predict feed isn't in corresponding train feeds" + s += f"\n predict feed = {predict_f}" + s += f"\n train feeds = {train_fs}" raise ValueError(s) def __str__(self): @@ -198,7 +193,7 @@ def mock_feed_ppss( @enforce_types def mock_ppss( - feeds: list, + feedset_list: list, network: Optional[str] = None, tmpdir: Optional[str] = None, st_timestr: Optional[str] = "2023-06-18", @@ -209,14 +204,14 @@ def mock_ppss( yaml_str = fast_test_yaml_str(tmpdir) ppss = PPSS(yaml_str=yaml_str, network=network) - predict_feeds = PredictFeeds.from_array(feeds) + predict_train_feedsets = PredictTrainFeedsets.from_list_of_dict(feedset_list) if tmpdir is None: tmpdir = tempfile.mkdtemp() assert hasattr(ppss, "lake_ss") ppss.lake_ss = LakeSS( { - "feeds": predict_feeds.feeds_str, + "feeds": predict_train_feedsets.feed_strs, "parquet_dir": os.path.join(tmpdir, "parquet_data"), "st_timestr": st_timestr, "fin_timestr": fin_timestr, @@ -225,14 +220,15 @@ def mock_ppss( assert hasattr(ppss, "predictoor_ss") d = predictoor_ss_test_dict( - predict_feeds=feeds, pred_submitter_mgr=pred_submitter_mgr + feedset_list=feedset_list, + pred_submitter_mgr=pred_submitter_mgr, ) ppss.predictoor_ss = PredictoorSS(d) assert hasattr(ppss, "trader_ss") ppss.trader_ss = TraderSS( { - "feed": predict_feeds.feeds_str[0], + "feed": predict_train_feedsets.feed_strs[0], "sim_only": { "buy_amt": "10 USD", }, @@ -249,12 +245,12 @@ def mock_ppss( assert hasattr(ppss, "trueval_ss") assert "feeds" in ppss.trueval_ss.d # type: ignore[attr-defined] - ppss.trueval_ss.d["feeds"] = predict_feeds.feeds_str # type: ignore[attr-defined] + ppss.trueval_ss.d["feeds"] = predict_train_feedsets.feed_strs # type: ignore[attr-defined] assert hasattr(ppss, "dfbuyer_ss") ppss.dfbuyer_ss = DFBuyerSS( { - "feeds": predict_feeds.feeds_str, + "feeds": predict_train_feedsets.feed_strs, "batch_size": 20, "consume_interval_seconds": 86400, "weekly_spending_limit": 37000, diff --git a/pdr_backend/ppss/predict_feed_mixin.py b/pdr_backend/ppss/predict_feed_mixin.py deleted file mode 100644 index 630086b2d..000000000 --- a/pdr_backend/ppss/predict_feed_mixin.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import Dict, List, Optional -from enforce_typing import enforce_types -from pdr_backend.cli.predict_feeds import PredictFeed, PredictFeeds -from pdr_backend.subgraph.subgraph_feed import SubgraphFeed - - -@enforce_types -class PredictFeedMixin: - FEEDS_KEY = "feeds" - - def __init__(self, d: dict, assert_feed_attributes: Optional[List] = None): - assert self.__class__.FEEDS_KEY - self.d = d - if assert_feed_attributes: - missing_attributes = [] - for attr in assert_feed_attributes: - for feed in self.feeds.feeds: - if not getattr(feed, attr): - missing_attributes.append(attr) - - if missing_attributes: - raise AssertionError( - f"Missing attributes {missing_attributes} for some feeds." - ) - - @property - def feeds(self) -> PredictFeeds: - return PredictFeeds.from_array(self.d.get(self.__class__.FEEDS_KEY, [])) - - @property - def minimum_timeframe_seconds(self) -> int: - min_tf_seconds = int(1e9) - for feed in self.feeds: - assert ( - feed.predict.timeframe is not None - ), f"Feed: {feed} is missing timeframe" - min_tf_seconds = min(min_tf_seconds, feed.predict.timeframe.s) - return min_tf_seconds - - def get_predict_feed(self, pair, timeframe, exchange) -> Optional[PredictFeed]: - for predict_feed in self.feeds: - p = predict_feed.predict - if p.pair == pair and p.timeframe == timeframe and p.exchange == exchange: - return predict_feed - return None - - @enforce_types - def get_feed_from_candidates( - self, cand_feeds: Dict[str, SubgraphFeed] - ) -> Dict[str, SubgraphFeed]: - result: Dict[str, SubgraphFeed] = {} - - allowed_tups = [ - (str(feed.exchange), str(feed.pair), str(feed.timeframe)) - for feed in self.feeds.feeds - ] - - for sg_key, sg_feed in cand_feeds.items(): - assert isinstance(sg_feed, SubgraphFeed) - - if (sg_feed.source, sg_feed.pair, sg_feed.timeframe) in allowed_tups: - result[sg_key] = sg_feed - - return result diff --git a/pdr_backend/ppss/predictoor_ss.py b/pdr_backend/ppss/predictoor_ss.py index b68337a4e..b401d74ab 100644 --- a/pdr_backend/ppss/predictoor_ss.py +++ b/pdr_backend/ppss/predictoor_ss.py @@ -1,10 +1,15 @@ +from typing import Dict, List, Optional + from enforce_typing import enforce_types -from pdr_backend.cli.predict_feeds import PredictFeeds +from pdr_backend.cli.predict_train_feedsets import ( + PredictTrainFeedset, + PredictTrainFeedsets, +) from pdr_backend.ppss.aimodel_ss import AimodelSS, aimodel_ss_test_dict -from pdr_backend.ppss.predict_feed_mixin import PredictFeedMixin -from pdr_backend.util.strutil import StrMixin +from pdr_backend.subgraph.subgraph_feed import SubgraphFeed from pdr_backend.util.currency_types import Eth +from pdr_backend.util.strutil import StrMixin # Approaches: # 1: Two-sided: Allocate up-vs-down stake equally (50-50). Baseline. @@ -13,22 +18,24 @@ CAND_APPROACHES = [1, 2, 3] -class PredictoorSS(PredictFeedMixin, StrMixin): +class PredictoorSS(StrMixin): __STR_OBJDIR__ = ["d"] - FEEDS_KEY = "feeds" @enforce_types def __init__(self, d: dict): - super().__init__(d, assert_feed_attributes=["timeframe"]) + self.d = d self.aimodel_ss = AimodelSS(d["aimodel_ss"]) + if self.approach not in CAND_APPROACHES: s = f"Allowed approaches={CAND_APPROACHES}, got {self.approach}" raise ValueError(s) - # -------------------------------- + # ------------------------------------------------------------------ # yaml properties - - # (predict_feeds defined in base) + @property + def predict_train_feedsets(self) -> PredictTrainFeedsets: + feedset_list: List[dict] = self.d["predict_train_feedsets"] + return PredictTrainFeedsets.from_list_of_dict(feedset_list) @property def approach(self) -> int: @@ -92,13 +99,64 @@ def set_approach(self, approach: int): raise ValueError(s) self.d["approach"] = approach + # ------------------------------------------------------------------ + # 'predict_train_feedsets' workers + @enforce_types + def get_predict_train_feedset( + self, + exchange_str: str, + pair_str: str, + timeframe_str: str, + ) -> Optional[PredictTrainFeedset]: + """Eg return a feedset given ("binance", "BTC/USDT", "5m" """ + return self.predict_train_feedsets.get_feedset( + exchange_str, + pair_str, + timeframe_str, + ) + + @enforce_types + def get_feed_from_candidates( + self, + cand_feeds: Dict[str, SubgraphFeed], + ) -> Dict[str, SubgraphFeed]: + """ + @description + Filter down the input cand_feeds to the ones we're supposed to predict + + More precisely: return a set of feeds as the intersection of + (1) candidate feeds read from chain, ie the input SubgraphFeeds, + and (2) self's feeds to predict, ie input by PPSS + + @arguments + cand_feeds -- dict of [feed_addr] : SubgraphFeed + + @return + filtered_feeds -- dict of [feed_addr] : SubgraphFeed + """ + filtered_feeds: Dict[str, SubgraphFeed] = {} + + allowed_tups = [ + (str(feed.exchange), str(feed.pair), str(feed.timeframe)) + for feed in self.predict_train_feedsets.feeds + ] + + for feed_addr, feed in cand_feeds.items(): + assert isinstance(feed, SubgraphFeed) + + if (feed.source, feed.pair, feed.timeframe) in allowed_tups: + filtered_feeds[feed_addr] = feed + + return filtered_feeds + # ========================================================================= # utilities for testing -def example_predict_feeds() -> list: - return [ +@enforce_types +def feedset_test_list() -> list: + feedset_list = [ { "predict": "binance BTC/USDT c 5m", "train_on": "binance BTC/USDT c 5m", @@ -108,19 +166,18 @@ def example_predict_feeds() -> list: "train_on": "kraken ETH/USDT c 5m", }, ] + return feedset_list @enforce_types def predictoor_ss_test_dict( - predict_feeds=None, - input_feeds=None, + feedset_list: Optional[List] = None, pred_submitter_mgr="", ) -> dict: """Use this function's return dict 'd' to construct PredictoorSS(d)""" - predict_feeds = predict_feeds or example_predict_feeds() - input_feeds = input_feeds or PredictFeeds.from_array(predict_feeds).feeds_str + feedset_list = feedset_list or feedset_test_list() d = { - "feeds": predict_feeds, + "predict_train_feedsets": feedset_list, "approach": 1, "stake_amount": 1, "pred_submitter_mgr": pred_submitter_mgr, @@ -131,7 +188,8 @@ def predictoor_ss_test_dict( }, "bot_only": { "s_until_epoch_end": 60, + "s_start_payouts": 0, }, - "aimodel_ss": aimodel_ss_test_dict(input_feeds), + "aimodel_ss": aimodel_ss_test_dict(), } return d diff --git a/pdr_backend/ppss/test/test_aimodel_ss.py b/pdr_backend/ppss/test/test_aimodel_ss.py index 99a31a670..d73bc68cb 100644 --- a/pdr_backend/ppss/test/test_aimodel_ss.py +++ b/pdr_backend/ppss/test/test_aimodel_ss.py @@ -1,29 +1,22 @@ -import re - from enforce_typing import enforce_types import numpy as np import pytest -from pdr_backend.cli.arg_feed import ArgFeed -from pdr_backend.cli.arg_feeds import ArgFeeds from pdr_backend.ppss.aimodel_ss import ( - APPROACH_OPTIONS, - WEIGHT_RECENT_OPTIONS, - BALANCE_CLASSES_OPTIONS, - CALIBRATE_PROBS_OPTIONS, AimodelSS, aimodel_ss_test_dict, + APPROACH_OPTIONS, + CALIBRATE_PROBS_OPTIONS, + BALANCE_CLASSES_OPTIONS, + WEIGHT_RECENT_OPTIONS, ) @enforce_types -def test_aimodel_ss_default_values(): +def test_aimodel_ss__default_values(): d = aimodel_ss_test_dict() ss = AimodelSS(d) - assert ss.feeds_strs == ["binance BTC/USDT c"] - assert ss.feeds == ArgFeeds([ArgFeed("binance", "close", "BTC/USDT")]) - assert ss.max_n_train == d["max_n_train"] == 7 assert ss.autoregressive_n == d["autoregressive_n"] == 3 @@ -38,19 +31,9 @@ def test_aimodel_ss_default_values(): @enforce_types -def test_aimodel_ss_nondefault_values(): - input_feeds = ["kraken ETH/USDT hc", "binance ETH/USDT TRX/DAI h"] - d = aimodel_ss_test_dict(input_feeds=input_feeds) +def test_aimodel_ss__nondefault_values(): + d = aimodel_ss_test_dict() ss = AimodelSS(d) - assert ss.feeds_strs == input_feeds - assert ss.feeds == ArgFeeds( - [ - ArgFeed("kraken", "high", "ETH/USDT"), - ArgFeed("kraken", "close", "ETH/USDT"), - ArgFeed("binance", "high", "ETH/USDT"), - ArgFeed("binance", "high", "TRX/DAI"), - ] - ) ss = AimodelSS(aimodel_ss_test_dict(max_n_train=39)) assert ss.max_n_train == 39 @@ -76,14 +59,7 @@ def test_aimodel_ss_nondefault_values(): @enforce_types -def test_aimodel_ss_unhappy_inputs(): - input_feeds = ["kraken ETH/USDT"] # missing eg "c" - d = aimodel_ss_test_dict(input_feeds) - with pytest.raises( - AssertionError, match=re.escape("Missing attributes ['signal'] for some feeds") - ): - AimodelSS(d) - +def test_aimodel_ss__bad_inputs(): with pytest.raises(ValueError): AimodelSS(aimodel_ss_test_dict(max_n_train=0)) diff --git a/pdr_backend/ppss/test/test_ppss.py b/pdr_backend/ppss/test/test_ppss.py index 345da9ccc..49091736f 100644 --- a/pdr_backend/ppss/test/test_ppss.py +++ b/pdr_backend/ppss/test/test_ppss.py @@ -5,10 +5,14 @@ from enforce_typing import enforce_types -from pdr_backend.cli.predict_feeds import PredictFeeds -from pdr_backend.ppss.predictoor_ss import example_predict_feeds - -from pdr_backend.ppss.ppss import PPSS, fast_test_yaml_str, mock_feed_ppss, mock_ppss +from pdr_backend.cli.predict_train_feedsets import PredictTrainFeedsets +from pdr_backend.ppss.ppss import ( + fast_test_yaml_str, + mock_feed_ppss, + mock_ppss, + PPSS, +) +from pdr_backend.ppss.predictoor_ss import feedset_test_list @enforce_types @@ -65,20 +69,21 @@ def test_mock_feed_ppss(): assert feed.source == "binance" assert feed.pair == "BTC/USDT" - assert str(ppss.predictoor_ss.feeds[0].predict) == "binance BTC/USDT c 5m" + predict_feed0 = ppss.predictoor_ss.predict_train_feedsets[0].predict + assert str(predict_feed0) == "binance BTC/USDT c 5m" assert ppss.lake_ss.feeds_strs == ["binance BTC/USDT c 5m"] assert ppss.web3_pp.network == "sapphire-mainnet" @enforce_types def test_mock_ppss_simple(): - ppss = mock_ppss(example_predict_feeds(), "sapphire-mainnet") + ppss = mock_ppss(feedset_test_list(), "sapphire-mainnet") assert ppss.web3_pp.network == "sapphire-mainnet" @enforce_types def test_mock_ppss_default_network_development(): - ppss = mock_ppss(example_predict_feeds()) + ppss = mock_ppss(feedset_test_list()) assert ppss.web3_pp.network == "development" @@ -105,10 +110,9 @@ def test_mock_ppss_onefeed1(feed_str): ppss = mock_ppss([{"predict": feed_str, "train_on": feed_str}], "sapphire-mainnet") assert ppss.lake_ss.d["feeds"] == [feed_str] - assert ppss.predictoor_ss.d["feeds"] == [ + assert ppss.predictoor_ss.d["predict_train_feedsets"] == [ {"predict": feed_str, "train_on": feed_str} ] - assert ppss.predictoor_ss.aimodel_ss.d["input_feeds"] == [feed_str] assert ppss.trader_ss.d["feed"] == feed_str assert ppss.trueval_ss.d["feeds"] == [feed_str] assert ppss.dfbuyer_ss.d["feeds"] == [feed_str] @@ -120,89 +124,76 @@ def test_mock_ppss_onefeed1(feed_str): def test_mock_ppss_manyfeed(): """Thorough test that the many-feed arg is used everywhere""" - feeds = [ + feedset_list = [ { "predict": "binance BTC/USDT ETH/USDT c 5m", "train_on": "binance BTC/USDT ETH/USDT c 5m", }, - {"predict": "kraken BTC/USDT c 5m", "train_on": "kraken BTC/USDT c 5m"}, + { + "predict": "kraken BTC/USDT c 5m", + "train_on": "kraken BTC/USDT c 5m", + }, ] - predict_feeds = PredictFeeds.from_array(feeds) - ppss = mock_ppss(feeds, "sapphire-mainnet") + ppss = mock_ppss(feedset_list, "sapphire-mainnet") - assert ppss.lake_ss.d["feeds"] == predict_feeds.feeds_str - assert ppss.predictoor_ss.d["feeds"] == feeds - assert ppss.predictoor_ss.aimodel_ss.d["input_feeds"] == predict_feeds.feeds_str - assert ppss.trader_ss.d["feed"] == predict_feeds.feeds_str[0] - assert ppss.trueval_ss.d["feeds"] == predict_feeds.feeds_str - assert ppss.dfbuyer_ss.d["feeds"] == predict_feeds.feeds_str + feedsets = PredictTrainFeedsets.from_list_of_dict(feedset_list) + assert ppss.lake_ss.d["feeds"] == feedsets.feed_strs + assert ppss.predictoor_ss.d["predict_train_feedsets"] == feedset_list + assert ppss.trader_ss.d["feed"] == feedsets.feed_strs[0] + assert ppss.trueval_ss.d["feeds"] == feedsets.feed_strs + assert ppss.dfbuyer_ss.d["feeds"] == feedsets.feed_strs ppss.verify_feed_dependencies() @enforce_types def test_verify_feed_dependencies(): + # create ppss ppss = mock_ppss( - example_predict_feeds(), + feedset_test_list(), "sapphire-mainnet", ) - ppss.verify_feed_dependencies() + assert "predict_train_feedsets" in ppss.predictoor_ss.d - # don't fail if aimodel needs more ohlcv feeds for same exchange/pair/time - ppss2 = deepcopy(ppss) - ppss2.predictoor_ss.aimodel_ss.d["input_feeds"] = PredictFeeds.from_array( - example_predict_feeds() - ).feeds_str - ppss2.verify_feed_dependencies() + # baseline should pass + ppss.verify_feed_dependencies() - # fail check: is predictoor_ss.predict_feed in lake feeds? + # fail check: does lake feeds hold each predict feed? Each train feed? # - check for matching {exchange, pair, timeframe} but not {signal} - assert "feeds" in ppss.predictoor_ss.d + good_feed = "binance BTC/USDT c 5m" for wrong_feed in [ - "binance BTC/USDT o 5m", - "binance ETH/USDT c 5m", - "binance BTC/USDT c 1h", - "kraken BTC/USDT c 5m", + "dydx BTC/USDT c 5m", # bad exchange + "binance DOT/USDT c 5m", # bad pair + "binance BTC/USDT c 1h", # bad timeframe ]: + # test lake <> predict feed ppss2 = deepcopy(ppss) - ppss2.predictoor_ss.d["feeds"] = [ - {"predict": wrong_feed, "train_on": wrong_feed} + ppss2.predictoor_ss.d["predict_train_feedsets"] = [ + {"predict": wrong_feed, "train_on": good_feed} ] with pytest.raises(ValueError): ppss2.verify_feed_dependencies() - # fail check: do all aimodel_ss input feeds conform to predict feed timeframe? - ppss2 = deepcopy(ppss) - ppss2.predictoor_ss.aimodel_ss.d["input_feeds"] = [ - "binance BTC/USDT c 5m", - "binance BTC/USDT c 1h", - ] # 0th ok, 1st bad - with pytest.raises(ValueError): - ppss2.verify_feed_dependencies() - - # fail check: is each predictoor_ss.aimodel_ss.input_feeds in lake feeds? - # - check for matching {exchange, pair, timeframe} but not {signal} - for wrong_feed in [ - "kraken BTC/USDT c 5m", - "binance ETH/USDT c 5m", - "binance BTC/USDT c 1h", - ]: - ppss2 = deepcopy(ppss) - ppss2.predictoor_ss.aimodel_ss.d["input_feeds"] = [wrong_feed] - with pytest.raises(ValueError): - ppss2.verify_feed_dependencies() - - # fail check: is predictoor_ss.predict_feed in aimodel_ss.input_feeds? - # - check for matching {exchange, pair, timeframe AND signal} - for wrong_feed in [ - "mexc BTC/USDT c 5m", - "binance DOT/USDT c 5m", - "binance BTC/USDT c 1h", - "binance BTC/USDT o 5m", - ]: + # test lake <> train feed ppss2 = deepcopy(ppss) - ppss2.predictoor_ss.d["feeds"] = [ - {"predict": wrong_feed, "train_on": wrong_feed} + ppss2.predictoor_ss.d["predict_train_feedsets"] = [ + {"predict": good_feed, "train_on": wrong_feed} ] with pytest.raises(ValueError): ppss2.verify_feed_dependencies() + + # fail check: do all feeds in predict/train sets have identical timeframe? + ppss2 = deepcopy(ppss) + ppss2.predictoor_ss.d["predict_train_feedsets"] = [ + {"predict": "binance BTC/USDT c 5m", "train_on": "binance BTC/USDT c 1h"} + ] + with pytest.raises(ValueError): + ppss2.verify_feed_dependencies() + + # fail check: is the predict feed in the corr. train feeds? + ppss2 = deepcopy(ppss) + ppss2.predictoor_ss.d["predict_train_feedsets"] = [ + {"predict": "binance BTC/USDT c 5m", "train_on": "binance ETH/USDT c 5m"} + ] + with pytest.raises(ValueError): + ppss2.verify_feed_dependencies() diff --git a/pdr_backend/ppss/test/test_predict_feed_mixin.py b/pdr_backend/ppss/test/test_predict_feed_mixin.py deleted file mode 100644 index 861a490a6..000000000 --- a/pdr_backend/ppss/test/test_predict_feed_mixin.py +++ /dev/null @@ -1,54 +0,0 @@ -from pdr_backend.cli.arg_feed import ArgFeed -from pdr_backend.cli.arg_feeds import ArgFeeds -from pdr_backend.ppss.predict_feed_mixin import PredictFeedMixin - - -def test_predict_feed_mixin(): - feed_dict = { - "feeds": [ - { - "predict": "binance BTC/USDT c 5m, kraken BTC/USDT c 5m", - "train_on": [ - "binance BTC/USDT ETH/USDT DOT/USDT c 5m", - "kraken BTC/USDT c 5m", - ], - }, - { - "predict": "binance ETH/USDT ADA/USDT c 5m", - "train_on": "binance BTC/USDT ETH/USDT DOT/USDT c 5m, kraken BTC/USDT c 5m", - }, - { - "predict": "binance BTC/USDT c 1h", - "train_on": "binance BTC/USDT ETH/USDT c 5m", - }, - ] - } - expected = [ - { - "predict": ArgFeed.from_str("binance BTC/USDT c 5m"), - "train_on": ArgFeeds.from_str("binance BTC/USDT ETH/USDT DOT/USDT c 5m") - + ArgFeeds.from_str("kraken BTC/USDT c 5m"), - }, - { - "predict": ArgFeed.from_str("kraken BTC/USDT c 5m"), - "train_on": ArgFeeds.from_str("binance BTC/USDT ETH/USDT DOT/USDT c 5m") - + ArgFeeds.from_str("kraken BTC/USDT c 5m"), - }, - { - "predict": ArgFeed.from_str("binance ETH/USDT c 5m"), - "train_on": ArgFeeds.from_str("binance BTC/USDT ETH/USDT DOT/USDT c 5m") - + ArgFeeds.from_str("kraken BTC/USDT c 5m"), - }, - { - "predict": ArgFeed.from_str("binance ADA/USDT c 5m"), - "train_on": ArgFeeds.from_str("binance BTC/USDT ETH/USDT DOT/USDT c 5m") - + ArgFeeds.from_str("kraken BTC/USDT c 5m"), - }, - { - "predict": ArgFeed.from_str("binance BTC/USDT c 1h"), - "train_on": ArgFeeds.from_str("binance BTC/USDT ETH/USDT c 5m"), - }, - ] - parser = PredictFeedMixin(feed_dict) - - assert parser.feeds.to_list() == expected diff --git a/pdr_backend/ppss/test/test_predictoor_ss.py b/pdr_backend/ppss/test/test_predictoor_ss.py index ee5cdd8d9..3fb10b6d8 100644 --- a/pdr_backend/ppss/test/test_predictoor_ss.py +++ b/pdr_backend/ppss/test/test_predictoor_ss.py @@ -7,31 +7,19 @@ @enforce_types -def test_predictoor_ss(): +def test_predictoor_ss_main(): # build PredictoorSS d = predictoor_ss_test_dict() - - assert "feeds" in d - d["feeds"] = [ - { - "predict": "binance BTC/USDT c 5m", - "train_on": "binance BTC/USDT c 5m", - } - ] - - assert "input_feeds" in d["aimodel_ss"] - d["aimodel_ss"]["input_feeds"] = [ - "binance BTC/USDT c 5m", - "kraken ETH/USDT o 1h", - ] ss = PredictoorSS(d) # test yaml properties - assert ss.feeds[0].predict == ArgFeed("binance", "close", "BTC/USDT", "5m") - assert ss.aimodel_ss.feeds == [ - ArgFeed("binance", "close", "BTC/USDT", "5m"), - ArgFeed("kraken", "open", "ETH/USDT", "1h"), - ] + feedsets = ss.predict_train_feedsets + assert len(feedsets) == 2 + f0, f1 = feedsets[0], feedsets[1] + assert f0.predict == ArgFeed("binance", "close", "BTC/USDT", "5m") + assert f0.train_on == [ArgFeed("binance", "close", "BTC/USDT", "5m")] + assert f1.predict == ArgFeed("kraken", "close", "ETH/USDT", "5m") + assert f1.train_on == [ArgFeed("kraken", "close", "ETH/USDT", "5m")] assert ss.approach == 1 assert ss.stake_amount == Eth(1) @@ -50,48 +38,48 @@ def test_predictoor_ss(): ss.set_approach(3) assert ss.approach == 3 + # test get_predict_train_feedset() + assert ss.get_predict_train_feedset("binance", "BTC/USDT", "5m") == f0 + assert ss.get_predict_train_feedset("foo", "BTC/USDT", "5m") is None -@enforce_types -def test_predictoor_ss_test_dict(): - # test - reasonable defaults when nothing passed in - d = predictoor_ss_test_dict() - f = d["feeds"] - assert type(f) == list +@enforce_types +def test_predictoor_ss_feedsets_in_test_dict(): # test 5m - predict_feeds = [ + feedset_list = [ { "predict": "binance ETH/USDT c 5m", "train_on": "binance ETH/USDT ADA/USDT c 5m", } ] - d = predictoor_ss_test_dict(predict_feeds) - assert d["feeds"] == predict_feeds - assert d["aimodel_ss"]["input_feeds"] == [ - "binance ETH/USDT c 5m", - "binance ADA/USDT c 5m", - ] + d = predictoor_ss_test_dict(feedset_list) + assert d["predict_train_feedsets"] == feedset_list # test 1h - predict_feeds = [ + feedset_list = [ { "predict": "binance ETH/USDT c 1h", "train_on": "binance ETH/USDT c 1h", } ] - d = predictoor_ss_test_dict(predict_feeds) - assert d["feeds"] == predict_feeds - assert d["aimodel_ss"]["input_feeds"] == ["binance ETH/USDT c 1h"] + d = predictoor_ss_test_dict(feedset_list) + assert d["predict_train_feedsets"] == feedset_list - # test s_start_payouts attribute set - ss = PredictoorSS(d) - assert ss.s_start_payouts == 0, "Must be unset in the test dict, so should return 0" +@enforce_types +def test_predictoor_ss_start_payouts(): + # use defaults + d = predictoor_ss_test_dict() + ss = PredictoorSS(d) + assert ss.s_start_payouts == 0 - # let's set it here + # explicitly set + d = predictoor_ss_test_dict() + assert "bot_only" in d + assert "s_start_payouts" in d["bot_only"] d["bot_only"]["s_start_payouts"] = 100 ss = PredictoorSS(d) - assert ss.s_start_payouts == 100, "Must be set to 100" + assert ss.s_start_payouts == 100 @enforce_types @@ -99,6 +87,7 @@ def test_predictoor_ss_bad_approach(): # catch bad approach in __init__() for bad_approach in [0, 4]: d = predictoor_ss_test_dict() + assert "approach" in d d["approach"] = bad_approach with pytest.raises(ValueError): PredictoorSS(d) diff --git a/pdr_backend/pred_submitter/test/test_pred_submitter_manager.py b/pdr_backend/pred_submitter/test/test_pred_submitter_manager.py index 64c2d6bec..f78750914 100644 --- a/pdr_backend/pred_submitter/test/test_pred_submitter_manager.py +++ b/pdr_backend/pred_submitter/test/test_pred_submitter_manager.py @@ -144,12 +144,12 @@ def test_submit_prediction_and_payout( bal_before = OCEAN.balanceOf(web3_config.owner) assert bal_before > Wei(100), "Not enough balance to execute the test" - feeds = [ + feed_addrs = [ feed_contract1.contract_address, feed_contract2.contract_address, ] # give allowance - tx_receipt = pred_submitter_mgr.approve_ocean(feeds) + tx_receipt = pred_submitter_mgr.approve_ocean(feed_addrs) assert tx_receipt.status == 1, "Transaction failed" # submit prediction @@ -158,7 +158,7 @@ def test_submit_prediction_and_payout( tx_receipt = pred_submitter_mgr.submit_prediction( stakes_up=[Wei(20), Wei(30)], stakes_down=[Wei(40), Wei(10)], - feeds=feeds, + feed_addrs=feed_addrs, epoch=prediction_epoch, wait_for_receipt=True, ) @@ -184,7 +184,7 @@ def test_submit_prediction_and_payout( bal_before = OCEAN.balanceOf(web3_config.owner) # claim - pred_submitter_mgr.get_payout([prediction_epoch], feeds, wait_for_receipt=True) + pred_submitter_mgr.get_payout([prediction_epoch], feed_addrs, wait_for_receipt=True) # get the OCEAN balance of the owner after claiming bal_after = OCEAN.balanceOf(web3_config.owner) diff --git a/pdr_backend/predictoor/predictoor_agent.py b/pdr_backend/predictoor/predictoor_agent.py index 6edaed2c4..16c4bf075 100644 --- a/pdr_backend/predictoor/predictoor_agent.py +++ b/pdr_backend/predictoor/predictoor_agent.py @@ -7,11 +7,12 @@ from pdr_backend.aimodel.aimodel_data_factory import AimodelDataFactory from pdr_backend.aimodel.aimodel_factory import AimodelFactory -from pdr_backend.cli.predict_feeds import PredictFeed +from pdr_backend.cli.predict_train_feedsets import PredictTrainFeedset from pdr_backend.contract.pred_submitter_mgr import PredSubmitterMgr from pdr_backend.contract.token import NativeToken, Token from pdr_backend.lake.ohlcv_data_factory import OhlcvDataFactory from pdr_backend.ppss.ppss import PPSS +from pdr_backend.predictoor.stakes_per_slot import StakeTup, StakesPerSlot from pdr_backend.subgraph.subgraph_feed import print_feeds, SubgraphFeed from pdr_backend.subgraph.subgraph_pending_payouts import query_pending_payouts from pdr_backend.util.currency_types import Eth, Wei @@ -19,35 +20,7 @@ from pdr_backend.util.time_types import UnixTimeS logger = logging.getLogger("predictoor_agent") - - -class PredictionSlotsData: - def __init__(self): - self.target_slots = {} - - def add_prediction(self, slot, feed, stake_up, stake_down): - if slot not in self.target_slots: - self.target_slots[slot] = [] - self.target_slots[slot].append((feed, stake_up, stake_down)) - - def get_predictions(self, slot): - return self.target_slots.get(slot, []) - - def get_predictions_arr(self, slot): - stakes_up = [] - stakes_down = [] - feed_addrs = [] - - for feed, stake_up, stake_down in self.get_predictions(slot): - stakes_up.append(stake_up) - stakes_down.append(stake_down) - feed_addrs.append(feed.address) - - return stakes_up, stakes_down, feed_addrs - - @property - def slots(self): - return list(self.target_slots.keys()) +MAX_WEI = Wei(2**256 - 1) # pylint: disable=too-many-public-methods @@ -76,16 +49,14 @@ def __init__(self, ppss: PPSS): self.ppss.web3_pp, pred_submitter_mgr_addr ) - # set self.feed cand_feeds: Dict[str, SubgraphFeed] = ppss.web3_pp.query_feed_contracts() - checksummed_addresses = [ - self.ppss.web3_pp.web3_config.w3.to_checksum_address(addr) - for addr in cand_feeds.keys() - ] - self.OCEAN.approve(self.pred_submitter_mgr.contract_address, Wei(2**256 - 1)) - self.pred_submitter_mgr.approve_ocean(checksummed_addresses) print_feeds(cand_feeds, f"cand feeds, owner={ppss.web3_pp.owner_addrs}") + feed_addrs: List[str] = list(cand_feeds.keys()) + feed_addrs = self._to_checksum(feed_addrs) + self.OCEAN.approve(self.pred_submitter_mgr.contract_address, MAX_WEI) + self.pred_submitter_mgr.approve_ocean(feed_addrs) + self.feeds: List[SubgraphFeed] = ppss.predictoor_ss.get_feed_from_candidates( cand_feeds ) @@ -114,19 +85,21 @@ def run(self): break @enforce_types - def prepare_stakes(self, feeds: List[SubgraphFeed]) -> PredictionSlotsData: - slot_data = PredictionSlotsData() + def calc_stakes_across_feeds(self, feeds: List[SubgraphFeed]) -> StakesPerSlot: + stakes = StakesPerSlot() seconds_per_epoch = None cur_epoch = None for feed in feeds: contract = self.ppss.web3_pp.get_single_contract(feed.address) - predict_pair = self.ppss.predictoor_ss.get_predict_feed( - feed.pair, feed.timeframe, feed.source + feedset = self.ppss.predictoor_ss.get_predict_train_feedset( + feed.source, + feed.pair, + feed.timeframe, ) - if predict_pair is None: - logger.error("No predict pair found for feed %s", feed) + if feedset is None: + logger.error("No (predict, train) pair found for feed %s", feed) seconds_per_epoch = feed.seconds_per_epoch cur_epoch = contract.get_current_epoch() next_slot = UnixTimeS((cur_epoch + 1) * seconds_per_epoch) @@ -139,11 +112,12 @@ def prepare_stakes(self, feeds: List[SubgraphFeed]) -> PredictionSlotsData: # get the target slot # get the stakes - stake_up, stake_down = self.calc_stakes(predict_pair) + stake_up, stake_down = self.calc_stakes(feedset) target_slot = UnixTimeS((cur_epoch + 2) * seconds_per_epoch) - slot_data.add_prediction(target_slot, feed, stake_up, stake_down) + tup = StakeTup(feed, stake_up, stake_down) + stakes.add_stake_at_slot(target_slot, tup) - return slot_data + return stakes @enforce_types def take_step(self): @@ -161,19 +135,16 @@ def take_step(self): self.prev_block_timestamp = UnixTimeS(self.cur_timestamp) # get payouts - # set predictoor_ss.bot_only.s_start_payouts to 0 to disable auto payouts self.get_payout() - slot_data = self.prepare_stakes(list(self.feeds.values())) + # for each feed, calculate up/down stake (eg via models) + feeds = list(self.feeds.values()) + stakes: StakesPerSlot = self.calc_stakes_across_feeds(feeds) - for target_slot in slot_data.slots: - stakes_up, stakes_down, feed_addrs = slot_data.get_predictions_arr( - target_slot - ) - feed_addrs = [ - self.ppss.web3_pp.web3_config.w3.to_checksum_address(addr) - for addr in feed_addrs - ] + # submit prediction txs + for target_slot in stakes.target_slots: + stakes_up, stakes_down, feed_addrs = stakes.get_stake_lists(target_slot) + feed_addrs = self._to_checksum(feed_addrs) required_OCEAN = Eth(0) for stake in stakes_up + stakes_down: @@ -183,14 +154,21 @@ def take_step(self): return logger.info(self.status_str()) - s = f"-> Predict result: {stakes_up} up, {stakes_down} down, feeds={feed_addrs}" + s = f"-> Predict result: {stakes_up} up, {stakes_down} down" + s += f", feeds={feed_addrs}" logger.info(s) - if required_OCEAN == 0: + + if required_OCEAN == Eth(0): logger.warning("Done: no predictions to submit") return - # submit prediction to chaineds] - self.submit_prediction_txs(stakes_up, stakes_down, target_slot, feed_addrs) + # submit prediction to chain + self.submit_prediction_txs( + stakes_up, + stakes_down, + target_slot, + feed_addrs, + ) self.prev_submit_epochs.append(target_slot) # start printing for next round @@ -199,6 +177,12 @@ def take_step(self): logger.info(self.status_str()) logger.info("Waiting...") + @enforce_types + def _to_checksum(self, addrs: List[str]) -> List[str]: + w3 = self.ppss.web3_pp.w3 + checksummed_addrs = [w3.to_checksum_address(addr) for addr in addrs] + return checksummed_addrs + @property def cur_block(self): return self.ppss.web3_pp.web3_config.get_block( @@ -218,7 +202,9 @@ def min_epoch_s_left(self): """ Returns the closest epoch time left in seconds """ - min_tf_seconds = self.ppss.predictoor_ss.feeds.min_epoch_seconds + min_tf_seconds = ( + self.ppss.predictoor_ss.predict_train_feedsets.min_epoch_seconds + ) current_ts = self.cur_timestamp seconds_left = min_tf_seconds - current_ts % min_tf_seconds return seconds_left @@ -229,7 +215,9 @@ def cur_unique_epoch(self): Returns the unique epoch number for the current timestamp """ t = self.cur_timestamp - min_tf_seconds = self.ppss.predictoor_ss.feeds.min_epoch_seconds + min_tf_seconds = ( + self.ppss.predictoor_ss.predict_train_feedsets.min_epoch_seconds + ) return t // min_tf_seconds @property @@ -265,7 +253,7 @@ def submit_prediction_txs( stakes_up: List[Eth], stakes_down: List[Eth], target_slot: UnixTimeS, # a timestamp - feeds: List[str], + feed_addrs: List[str], ): logger.info("Submitting predictions to the chain...") stakes_up_wei = [i.to_wei() for i in stakes_up] @@ -273,7 +261,7 @@ def submit_prediction_txs( tx = self.pred_submitter_mgr.submit_prediction( stakes_up=stakes_up_wei, stakes_down=stakes_down_wei, - feeds=feeds, + feed_addrs=feed_addrs, epoch=target_slot, ) logger.info("Tx submitted %s", tx["transactionHash"].hex()) @@ -285,7 +273,7 @@ def submit_prediction_txs( logger.warning(s) @enforce_types - def calc_stakes(self, feed: PredictFeed) -> Tuple[Eth, Eth]: + def calc_stakes(self, feedset: PredictTrainFeedset) -> Tuple[Eth, Eth]: """ @return stake_up -- amt to stake up, in units of Eth @@ -295,9 +283,9 @@ def calc_stakes(self, feed: PredictFeed) -> Tuple[Eth, Eth]: if approach == 1: return self.calc_stakes1() if approach == 2: - return self.calc_stakes2(feed) + return self.calc_stakes2(feedset) if approach == 3: - return self.calc_stakes3(feed) + return self.calc_stakes3(feedset) raise ValueError("Approach not supported") @enforce_types @@ -317,7 +305,7 @@ def calc_stakes1(self) -> Tuple[Eth, Eth]: return (stake_up, stake_down) @enforce_types - def calc_stakes2(self, feed: PredictFeed) -> Tuple[Eth, Eth]: + def calc_stakes2(self, feedset: PredictTrainFeedset) -> Tuple[Eth, Eth]: """ @description Calculate up-vs-down stake according to approach 2. @@ -328,10 +316,11 @@ def calc_stakes2(self, feed: PredictFeed) -> Tuple[Eth, Eth]: stake_down -- amt to stake down, "" """ assert self.ppss.predictoor_ss.approach == 2 - (stake_up, stake_down) = self.calc_stakes_2ss_model(feed) + (stake_up, stake_down) = self.calc_stakes_2ss_model(feedset) return (stake_up, stake_down) - def calc_stakes3(self, feed) -> Tuple[Eth, Eth]: + @enforce_types + def calc_stakes3(self, feedset) -> Tuple[Eth, Eth]: """ @description Calculate up-vs-down stake according to approach 3. @@ -342,7 +331,7 @@ def calc_stakes3(self, feed) -> Tuple[Eth, Eth]: stake_down -- amt to stake down, "" """ assert self.ppss.predictoor_ss.approach == 3 - (stake_up, stake_down) = self.calc_stakes_2ss_model(feed) + (stake_up, stake_down) = self.calc_stakes_2ss_model(feedset) if stake_up == stake_down: return (Eth(0), Eth(0)) @@ -353,7 +342,7 @@ def calc_stakes3(self, feed) -> Tuple[Eth, Eth]: return (Eth(0), stake_down - stake_up) @enforce_types - def calc_stakes_2ss_model(self, feed) -> Tuple[Eth, Eth]: + def calc_stakes_2ss_model(self, feedset) -> Tuple[Eth, Eth]: """ @description Model-based calculate up-vs-down stake. @@ -367,7 +356,10 @@ def calc_stakes_2ss_model(self, feed) -> Tuple[Eth, Eth]: data_f = AimodelDataFactory(self.ppss.predictoor_ss) X, ycont, _, xrecent = data_f.create_xy( - mergedohlcv_df, testshift=0, feed=feed.predict, feeds=feed.train_on + mergedohlcv_df, + testshift=0, + predict_feed=feedset.predict, + train_feeds=feedset.train_on, ) curprice = ycont[-1] @@ -421,6 +413,7 @@ def check_balances(self, required_OCEAN: Eth) -> bool: return True + @enforce_types def get_payout(self): """Claims payouts""" if ( @@ -432,6 +425,7 @@ def get_payout(self): logger.info(self.status_str()) logger.info("Running payouts") + # Update previous payouts history to avoid claiming for this epoch again self.prev_submit_payouts.append(self.cur_unique_epoch) diff --git a/pdr_backend/predictoor/stakes_per_slot.py b/pdr_backend/predictoor/stakes_per_slot.py new file mode 100644 index 000000000..80d694399 --- /dev/null +++ b/pdr_backend/predictoor/stakes_per_slot.py @@ -0,0 +1,68 @@ +from typing import Dict, List, Tuple + +from enforce_typing import enforce_types + +from pdr_backend.subgraph.subgraph_feed import SubgraphFeed +from pdr_backend.util.currency_types import Eth +from pdr_backend.util.time_types import UnixTimeS + + +@enforce_types +class StakeTup: + def __init__(self, feed: SubgraphFeed, stake_up: Eth, stake_down: Eth): + self.feed = feed + self.stake_up = stake_up + self.stake_down = stake_down + + def __eq__(self, other): + return ( + self.feed == other.feed + and self.stake_up == other.stake_up + and self.stake_down == other.stake_down + ) + + +@enforce_types +class StakeTups(List[StakeTup]): + pass + + +class StakesPerSlot: + def __init__(self): + self.target_slots: Dict[UnixTimeS, StakeTups] = {} + + @property + def slots(self) -> List[UnixTimeS]: + """@return -- list of timeslots handled so far""" + return list(self.target_slots.keys()) + + @enforce_types + def add_stake_at_slot( + self, + timeslot: UnixTimeS, + stake_tup: StakeTup, + ): + if timeslot not in self.target_slots: + self.target_slots[timeslot] = StakeTups([]) + self.target_slots[timeslot].append(stake_tup) + + @enforce_types + def get_stakes_at_slot(self, timeslot: UnixTimeS) -> StakeTups: + """@return -- predictions at the specified timeslot""" + tups = self.target_slots.get(timeslot, StakeTups([])) + return tups + + @enforce_types + def get_stake_lists( + self, timeslot: UnixTimeS + ) -> Tuple[List[Eth], List[Eth], List[str]]: + stakes_up: List[Eth] = [] + stakes_down: List[Eth] = [] + feed_addrs: List[str] = [] + + for tup in self.get_stakes_at_slot(timeslot): + stakes_up.append(tup.stake_up) + stakes_down.append(tup.stake_down) + feed_addrs.append(tup.feed.address) + + return stakes_up, stakes_down, feed_addrs diff --git a/pdr_backend/predictoor/test/test_predictoor_agent.py b/pdr_backend/predictoor/test/test_predictoor_agent.py index 81f9ad6b5..343b54e92 100644 --- a/pdr_backend/predictoor/test/test_predictoor_agent.py +++ b/pdr_backend/predictoor/test/test_predictoor_agent.py @@ -159,11 +159,8 @@ def __init__(self): self.last_yptrue = None # "" def predict_ptrue(self, X: np.ndarray) -> np.ndarray: - ar_n = self.aimodel_ss.autoregressive_n - n_feeds = self.aimodel_ss.n_feeds - (n_points, n_vars) = X.shape + (n_points, _) = X.shape assert n_points == 1 # this mock can only handle 1 input point - assert n_vars == self.aimodel_ss.n == ar_n * n_feeds CLOSE_VALS = X prob_up = np.sum(CLOSE_VALS) / 1e6 @@ -202,12 +199,11 @@ def mock_build(*args, **kwargs): # pylint: disable=unused-argument 2, str(tmpdir), monkeypatch, pred_submitter_mgr.contract_address ) aimodel_ss = ppss.predictoor_ss.aimodel_ss - assert aimodel_ss.n_feeds == 1 # do prediction mock_model.aimodel_ss = aimodel_ss agent = PredictoorAgent(ppss) - feed = ppss.predictoor_ss.feeds[0] + feed = ppss.predictoor_ss.predict_train_feedsets[0] agent.calc_stakes2(feed) ar_n = aimodel_ss.autoregressive_n @@ -246,12 +242,11 @@ def mock_build(*args, **kwargs): # pylint: disable=unused-argument assert len(feeds) == 2 aimodel_ss = ppss.predictoor_ss.aimodel_ss - assert aimodel_ss.n_feeds == 2 # do prediction mock_model.aimodel_ss = aimodel_ss agent = PredictoorAgent(ppss) - feed = ppss.predictoor_ss.feeds[0] + feed = ppss.predictoor_ss.predict_train_feedsets[0] agent.calc_stakes2(feed) ar_n = aimodel_ss.autoregressive_n diff --git a/pdr_backend/predictoor/test/test_stakes_per_slot.py b/pdr_backend/predictoor/test/test_stakes_per_slot.py new file mode 100644 index 000000000..952393cf5 --- /dev/null +++ b/pdr_backend/predictoor/test/test_stakes_per_slot.py @@ -0,0 +1,79 @@ +from unittest.mock import Mock + +from enforce_typing import enforce_types + +from pdr_backend.predictoor.stakes_per_slot import ( + StakeTup, + StakeTups, + StakesPerSlot, +) +from pdr_backend.subgraph.subgraph_feed import SubgraphFeed +from pdr_backend.util.currency_types import Eth +from pdr_backend.util.time_types import UnixTimeS + +FEED0 = Mock(spec=SubgraphFeed) +FEED1 = Mock(spec=SubgraphFeed) +FEED0.address = "0xFeed0" +FEED1.address = "0xFeed1" + +STAKE_UP0, STAKE_DOWN0 = Eth(10.0), Eth(20.0) +STAKE_UP1, STAKE_DOWN1 = Eth(11.0), Eth(21.0) +STAKE_UP2, STAKE_DOWN2 = Eth(12.0), Eth(22.0) +STAKE_UP3, STAKE_DOWN3 = Eth(13.0), Eth(23.0) + +TUP0 = StakeTup(FEED0, STAKE_UP0, STAKE_DOWN0) +TUP1 = StakeTup(FEED0, STAKE_UP1, STAKE_DOWN1) +TUP2 = StakeTup(FEED1, STAKE_UP2, STAKE_DOWN2) +TUP3 = StakeTup(FEED1, STAKE_UP3, STAKE_DOWN3) + +TIMESLOT0 = UnixTimeS(1000) +TIMESLOT1 = UnixTimeS(2000) + + +@enforce_types +def test_StakeTup(): + assert TUP0.feed == FEED0 + assert TUP0.stake_up == STAKE_UP0 + assert TUP0.stake_down == STAKE_DOWN0 + + +@enforce_types +def test_StakeTups(): + tups = StakeTups([TUP0, TUP1]) + assert tups[0] == TUP0 + assert tups[1] == TUP1 + + +@enforce_types +def test_StakesPerSlot(): + # empty to start + stakes = StakesPerSlot() + assert stakes.target_slots == {} + assert stakes.slots == [] + assert stakes.get_stakes_at_slot(TIMESLOT0) == [] + assert stakes.get_stakes_at_slot(TIMESLOT1) == [] + + # add at one timeslot + stakes.add_stake_at_slot(TIMESLOT0, TUP0) + stakes.add_stake_at_slot(TIMESLOT0, TUP1) + assert stakes.slots == [TIMESLOT0] + assert stakes.get_stakes_at_slot(TIMESLOT0) == [TUP0, TUP1] + assert stakes.get_stakes_at_slot(TIMESLOT1) == [] + + # add at another timeslot + stakes.add_stake_at_slot(TIMESLOT1, TUP2) + stakes.add_stake_at_slot(TIMESLOT1, TUP3) + assert stakes.slots == [TIMESLOT0, TIMESLOT1] + assert stakes.get_stakes_at_slot(TIMESLOT0) == [TUP0, TUP1] + assert stakes.get_stakes_at_slot(TIMESLOT1) == [TUP2, TUP3] + + # test get_stake_lists + (stakes_up, stakes_down, feed_addrs) = stakes.get_stake_lists(TIMESLOT0) + assert stakes_up == [STAKE_UP0, STAKE_UP1] + assert stakes_down == [STAKE_DOWN0, STAKE_DOWN1] + assert feed_addrs == ["0xFeed0", "0xFeed0"] + + (stakes_up, stakes_down, feed_addrs) = stakes.get_stake_lists(TIMESLOT1) + assert stakes_up == [STAKE_UP2, STAKE_UP3] + assert stakes_down == [STAKE_DOWN2, STAKE_DOWN3] + assert feed_addrs == ["0xFeed1", "0xFeed1"] diff --git a/pdr_backend/sim/multisim_engine.py b/pdr_backend/sim/multisim_engine.py index b25105cbe..71d7f132e 100644 --- a/pdr_backend/sim/multisim_engine.py +++ b/pdr_backend/sim/multisim_engine.py @@ -51,8 +51,9 @@ def run(self): point_i = self.ss.point_i(run_i) logger.info("Multisim run_i=%s: start. Vals=%s", run_i, point_i) ppss = self.ppss_from_point(point_i) - feed = ppss.predictoor_ss.feeds[0] - sim_engine = SimEngine(ppss, feed=feed, multi_id=str(uuid.uuid4())) + feedset = ppss.predictoor_ss.predict_train_feedsets[0] + multi_id = str(uuid.uuid4()) + sim_engine = SimEngine(ppss, feedset, multi_id) sim_engine.run() run_metrics = sim_engine.st.recent_metrics() self.update_csv(run_i, run_metrics, point_i) diff --git a/pdr_backend/sim/sim_engine.py b/pdr_backend/sim/sim_engine.py index 08a062ab2..16eb3a12e 100644 --- a/pdr_backend/sim/sim_engine.py +++ b/pdr_backend/sim/sim_engine.py @@ -14,9 +14,11 @@ from pdr_backend.aimodel.aimodel_data_factory import AimodelDataFactory from pdr_backend.aimodel.aimodel_factory import AimodelFactory from pdr_backend.aimodel.aimodel_plotdata import AimodelPlotdata +from pdr_backend.cli.arg_feed import ArgFeed +from pdr_backend.cli.arg_timeframe import ArgTimeframe +from pdr_backend.cli.predict_train_feedsets import PredictTrainFeedset from pdr_backend.exchange.exchange_mgr import ExchangeMgr from pdr_backend.lake.ohlcv_data_factory import OhlcvDataFactory -from pdr_backend.cli.predict_feeds import PredictFeed from pdr_backend.ppss.ppss import PPSS from pdr_backend.sim.sim_plotter import SimPlotter from pdr_backend.sim.sim_state import SimState @@ -28,14 +30,16 @@ # pylint: disable=too-many-instance-attributes class SimEngine: @enforce_types - def __init__(self, ppss: PPSS, feed: PredictFeed, multi_id: Optional[str] = None): - self.feed = feed - - # timeframe doesn't need to match - assert ( - str(feed.predict.exchange), - str(feed.predict.pair), - ) in ppss.predictoor_ss.aimodel_ss.exchange_pair_tups + def __init__( + self, + ppss: PPSS, + predict_train_feedset: PredictTrainFeedset, + multi_id: Optional[str] = None, + ): + self.predict_train_feedset = predict_train_feedset + assert isinstance(self.predict_feed, ArgFeed) + assert isinstance(self.tokcoin, str) + assert isinstance(self.usdcoin, str) self.ppss = ppss @@ -58,21 +62,19 @@ def __init__(self, ppss: PPSS, feed: PredictFeed, multi_id: Optional[str] = None else: self.multi_id = str(uuid.uuid4()) + @property + def predict_feed(self) -> ArgFeed: + return self.predict_train_feedset.predict + @property def tokcoin(self) -> str: """Return e.g. 'ETH'""" - base_str = self.feed.predict_base_str - if base_str is None: - raise ValueError("base_str is None") - return base_str + return self.predict_feed.pair.base_str @property def usdcoin(self) -> str: """Return e.g. 'USDT'""" - quote_str = self.feed.predict_quote_str - if quote_str is None: - raise ValueError("quote_str is None") - return quote_str + return self.predict_feed.pair.quote_str @enforce_types def _init_loop_attributes(self): @@ -111,8 +113,13 @@ def run_one_iter(self, test_i: int, mergedohlcv_df: pl.DataFrame): testshift = ppss.sim_ss.test_n - test_i - 1 # eg [99, 98, .., 2, 1, 0] data_f = AimodelDataFactory(pdr_ss) # type: ignore[arg-type] + predict_feed = self.predict_train_feedset.predict + train_feeds = self.predict_train_feedset.train_on X, ycont, x_df, _ = data_f.create_xy( - mergedohlcv_df, testshift, feed=self.feed.predict + mergedohlcv_df, + testshift, + predict_feed, + train_feeds, ) colnames = list(x_df.columns) @@ -132,7 +139,8 @@ def run_one_iter(self, test_i: int, mergedohlcv_df: pl.DataFrame): # current time recent_ut = UnixTimeMs(int(mergedohlcv_df["timestamp"].to_list()[-1])) - ut = UnixTimeMs(recent_ut - testshift * self.feed.timeframe_ms) + timeframe: ArgTimeframe = predict_feed.timeframe # type: ignore + ut = UnixTimeMs(recent_ut - testshift * timeframe.ms) # predict price direction prob_up: float = model.predict_ptrue(X_test)[0] # in [0.0, 1.0] @@ -265,7 +273,7 @@ def _buy(self, price: float, usdcoin_amt_send: float) -> float: self.st.holdings[self.tokcoin] += tokcoin_amt_recd self.exchange.create_market_buy_order( - self.feed.predict_pair_str, tokcoin_amt_recd + str(self.predict_feed.pair), tokcoin_amt_recd ) logger.info( @@ -302,7 +310,7 @@ def _sell(self, price: float, tokcoin_amt_send: float) -> float: self.st.holdings[self.usdcoin] += usdcoin_amt_recd self.exchange.create_market_sell_order( - self.feed.predict_pair_str, tokcoin_amt_send + str(self.predict_feed.pair), tokcoin_amt_send ) logger.info( diff --git a/pdr_backend/sim/test/test_multisim_engine.py b/pdr_backend/sim/test/test_multisim_engine.py index 150abaf7b..9b0214b36 100644 --- a/pdr_backend/sim/test/test_multisim_engine.py +++ b/pdr_backend/sim/test/test_multisim_engine.py @@ -36,28 +36,34 @@ def test_multisim1(tmpdir): @enforce_types def _constructor_d_with_fast_runtime(tmpdir): s = fast_test_yaml_str(tmpdir) - constructor_d = PPSS.constructor_dict(yaml_str=s) + main_d = PPSS.constructor_dict(yaml_str=s) - predict_feed = "binanceus BTC/USDT c 5m" - input_feeds = [predict_feed] + feed_s = "binanceus BTC/USDT c 5m" + input_feeds = [feed_s] + feedset_list = [{"train_on": feed_s, "predict": feed_s}] # lake ss parquet_dir = os.path.join(tmpdir, "parquet_data") - d = lake_ss_test_dict(parquet_dir, input_feeds) - d["st_timestr"] = "2023-06-18" - d["fin_timestr"] = "2023-06-19" - constructor_d["lake_ss"] = d + lake_d = lake_ss_test_dict(parquet_dir, input_feeds) + assert "st_timestr" in lake_d + assert "fin_timestr" in lake_d + lake_d["st_timestr"] = "2023-06-18" + lake_d["fin_timestr"] = "2023-06-19" + assert "lake_ss" in main_d + main_d["lake_ss"] = lake_d # predictoor ss - d = predictoor_ss_test_dict( - [{"train_on": predict_feed, "predict": predict_feed}], input_feeds - ) - d["aimodel_ss"]["max_n_train"] = 100 - constructor_d["predictoor_ss"] = d + pdr_d = predictoor_ss_test_dict(feedset_list) + assert "aimodel_ss" in pdr_d + assert "max_n_train" in pdr_d["aimodel_ss"] + pdr_d["aimodel_ss"]["max_n_train"] = 100 + assert "predictoor_ss" in main_d + main_d["predictoor_ss"] = pdr_d # sim ss log_dir = os.path.join(tmpdir, "logs") - d = sim_ss_test_dict(log_dir=log_dir, test_n=10) - constructor_d["sim_ss"] = d + sim_d = sim_ss_test_dict(log_dir=log_dir, test_n=10) + assert "sim_ss" in main_d + main_d["sim_ss"] = sim_d - return constructor_d + return main_d diff --git a/pdr_backend/sim/test/test_sim_engine.py b/pdr_backend/sim/test/test_sim_engine.py index e7bb4583d..a344f9778 100644 --- a/pdr_backend/sim/test/test_sim_engine.py +++ b/pdr_backend/sim/test/test_sim_engine.py @@ -1,6 +1,6 @@ import os from enforce_typing import enforce_types -from pdr_backend.cli.predict_feeds import PredictFeeds +from pdr_backend.cli.predict_train_feedsets import PredictTrainFeedsets from pdr_backend.ppss.lake_ss import LakeSS, lake_ss_test_dict from pdr_backend.ppss.ppss import PPSS, fast_test_yaml_str @@ -14,19 +14,28 @@ def test_sim_engine(tmpdir): s = fast_test_yaml_str(tmpdir) ppss = PPSS(yaml_str=s, network="development") - predict_feeds = [ - {"predict": "binanceus BTC/USDT c 5m", "train_on": "binanceus BTC/USDT c 5m"} + # set feeds; we'll use them below + feedset_list = [ + { + "predict": "binanceus BTC/USDT c 5m", + "train_on": "binanceus BTC/USDT c 5m", + } ] - predict_feeds_obj = PredictFeeds.from_array(predict_feeds) + feedsets = PredictTrainFeedsets.from_list_of_dict(feedset_list) + # lake ss parquet_dir = os.path.join(tmpdir, "parquet_data") - d = lake_ss_test_dict(parquet_dir, predict_feeds_obj.feeds_str) + d = lake_ss_test_dict(parquet_dir, feeds=feedsets.feed_strs) + assert "st_timestr" in d d["st_timestr"] = "2023-06-18" d["fin_timestr"] = "2023-06-19" ppss.lake_ss = LakeSS(d) # predictoor ss - d = predictoor_ss_test_dict(predict_feeds, predict_feeds_obj.feeds_str) + d = predictoor_ss_test_dict(feedset_list) + assert "approach" in d["aimodel_ss"] + assert "max_n_train" in d["aimodel_ss"] + assert "autoregressive_n" in d["aimodel_ss"] d["aimodel_ss"]["approach"] = "LinearLogistic" d["aimodel_ss"]["max_n_train"] = 20 d["aimodel_ss"]["autoregressive_n"] = 1 @@ -38,8 +47,8 @@ def test_sim_engine(tmpdir): ppss.sim_ss = SimSS(d) # go - feed = ppss.predictoor_ss.feeds[0] - sim_engine = SimEngine(ppss, feed=feed) + feedsets = ppss.predictoor_ss.predict_train_feedsets + sim_engine = SimEngine(ppss, feedsets[0]) sim_engine.run() # after implementing the P/C architeture, check state saved diff --git a/ppss.yaml b/ppss.yaml index beb597a53..0190b6f13 100644 --- a/ppss.yaml +++ b/ppss.yaml @@ -9,7 +9,7 @@ lake_ss: fin_timestr: now # ending date for data predictoor_ss: - feeds: + predict_train_feedsets: - predict: binance BTC/USDT ETH/USDT c 5m train_on: - binance BTC/USDT ETH/USDT c 5m @@ -22,12 +22,10 @@ predictoor_ss: revenue: 0.93006 # Sales revenue going towards predictoors. In OCEAN per epoch, per feed. Calcs: 37500 OCEAN/week/20_feeds (Predictoor DF rewards) = 18.6012 OCEAN/epoch/20_feeds (bc 2016 5m epochs/week) = 0.93006 OCEAN/epoch/feed bot_only: - s_until_epoch_end: 60 # in s. Start predicting if there's > this time left - s_start_payouts: 220 # in s. Run payout when there's this seconds left, set to 0 to disable + s_start_payouts: 220 # in s. Run payout if > this time left. 0 to disable + s_until_epoch_end: 60 # in s. Start predicting if > this time left aimodel_ss: - input_feeds: - - binance BTC/USDT ETH/USDT c 5m max_n_train: 5000 # no. epochs to train model on autoregressive_n: 1 # no. epochs that model looks back, to predict next approach: LinearLogistic # LinearLogistic | LinearSVC | Constant diff --git a/setup.py b/setup.py index bed72fce4..2f9d82290 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ "web3==6.17.1", "sapphire.py==0.2.2", "streamlit==1.32.2", + "typeguard==4.2.1", "ocean-contracts==2.0.4", # install this last ]