Skip to content

Commit 1c15390

Browse files
Update Staging (#100) (#101)
* Enhancement/increased evaluation window (#91) * update the rewards to be calculated with the last 6 hours of predictions * make evaluation window a config option * update reward to properly get 6 hours of past data offset by one hour ago. * add completeness to rewards calculation * Just multiply the current rewards by the completeness score * remove config options and introduce constants for the evaluation window * release notes and version incremementing --------- Co-authored-by: Peter | Yuma, a DCG Company <peter@yumaai.com>
1 parent df9bccb commit 1c15390

File tree

9 files changed

+110
-35
lines changed

9 files changed

+110
-35
lines changed

docs/Release Notes.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Release Notes
22
=============
33

4+
2.3.0
5+
-----
6+
Released on March 10th 2025
7+
- Increase the evaluation window to include 6 hours of miner predictions
8+
49
2.2.2
510
-----
611
Released on March 5th 2025

precog/constants/__init__.py

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
# Project Constants
22

3+
# Wandb Constants
34
WANDB_PROJECT = "yumaai"
5+
6+
# Predictions Constants
7+
EVALUATION_WINDOW_HOURS: int = 6
8+
PREDICTION_FUTURE_HOURS: int = 1
9+
PREDICTION_INTERVAL_MINUTES: int = 5

precog/utils/classes.py

+33-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timedelta
1+
from datetime import timedelta
22
from typing import List
33

44
from precog.utils.timestamp import get_now, get_timezone, round_minute_down, to_datetime
@@ -33,28 +33,47 @@ def clear_old_predictions(self):
3333
self.intervals = filtered_interval_dict
3434

3535
def format_predictions(self, reference_timestamp=None, hours: int = 1):
36-
# intervals = []
36+
"""
37+
Filter and format prediction and interval data based on a reference timestamp and time window.
38+
39+
This function filters the prediction and interval dictionaries to include only entries
40+
within a specified time window, ending at the reference timestamp and extending back
41+
by the specified number of hours.
42+
43+
Parameters:
44+
-----------
45+
reference_timestamp : datetime or str, optional
46+
The end timestamp for the time window. If None, the current time rounded down
47+
to the nearest minute is used. If a string is provided, it will be converted
48+
to a datetime object.
49+
hours : int, default=1
50+
The number of hours to look back from the reference timestamp.
51+
52+
Returns:
53+
--------
54+
tuple
55+
A tuple containing two dictionaries:
56+
- filtered_pred_dict: Dictionary of filtered predictions where keys are timestamps
57+
and values are the corresponding prediction values.
58+
- filtered_interval_dict: Dictionary of filtered intervals where keys are timestamps
59+
and values are the corresponding interval values.
60+
61+
Notes:
62+
------
63+
The actual time window used is (hours + 1) to ensure complete coverage of the requested period.
64+
"""
3765
if reference_timestamp is None:
3866
reference_timestamp = round_minute_down(get_now())
3967
if isinstance(reference_timestamp, str):
4068
reference_timestamp = to_datetime(reference_timestamp)
69+
4170
start_time = round_minute_down(reference_timestamp) - timedelta(hours=hours + 1)
71+
4272
filtered_pred_dict = {
4373
key: value for key, value in self.predictions.items() if start_time <= key <= reference_timestamp
4474
}
4575
filtered_interval_dict = {
4676
key: value for key, value in self.intervals.items() if start_time <= key <= reference_timestamp
4777
}
48-
return filtered_pred_dict, filtered_interval_dict
4978

50-
def get_relevant_timestamps(self, reference_timestamp: datetime):
51-
# returns a list of aligned timestamps
52-
# round down reference to nearest 5m
53-
round_down_now = round_minute_down(reference_timestamp)
54-
# get the timestamps for the past 12 epochs
55-
timestamps = [round_down_now - timedelta(minutes=5 * i) for i in range(12)]
56-
# remove any timestamps that are not in the dicts
57-
filtered_list = [
58-
item for item in timestamps if item in self.predictions.keys() and item in self.intervals.keys()
59-
]
60-
return filtered_list
79+
return filtered_pred_dict, filtered_interval_dict

precog/utils/config.py

-4
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,6 @@ def add_validator_args(parser):
233233
default="opentensor-dev",
234234
)
235235

236-
parser.add_argument("--prediction_interval", type=int, default=5)
237-
238-
parser.add_argument("--N_TIMEPOINTS", type=int, default=12)
239-
240236
parser.add_argument("--reset_state", action="store_true", dest="reset_state", help="Overwrites the state file")
241237

242238

precog/utils/wandb.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def log_wandb(responses, rewards, miner_uids, hotkeys):
4040
},
4141
}
4242

43-
bt.logging.debug(f"Attempting to log data to wandb: {wandb_val_log}")
43+
bt.logging.trace(f"Attempting to log data to wandb: {wandb_val_log}")
4444
wandb.log(wandb_val_log)
4545
except Exception as e:
4646
bt.logging.error(f"Failed to log to wandb: {str(e)}")

precog/validators/reward.py

+61-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy as np
55
from pandas import DataFrame
66

7+
from precog import constants
78
from precog.protocol import Challenge
89
from precog.utils.cm_data import CMData
910
from precog.utils.general import pd_to_dict, rank
@@ -15,42 +16,91 @@ def calc_rewards(
1516
self,
1617
responses: List[Challenge],
1718
) -> np.ndarray:
19+
evaluation_window_hours = constants.EVALUATION_WINDOW_HOURS
20+
prediction_future_hours = constants.PREDICTION_FUTURE_HOURS
21+
prediction_interval_minutes = constants.PREDICTION_INTERVAL_MINUTES
22+
23+
expected_timepoints = evaluation_window_hours * 60 / prediction_interval_minutes
24+
1825
# preallocate
1926
point_errors = []
2027
interval_errors = []
28+
completeness_scores = []
2129
decay = 0.9
2230
weights = np.linspace(0, len(self.available_uids) - 1, len(self.available_uids))
2331
decayed_weights = decay**weights
2432
timestamp = responses[0].timestamp
33+
bt.logging.debug(f"Calculating rewards for timestamp: {timestamp}")
2534
cm = CMData()
26-
start_time: str = to_str(get_before(timestamp=timestamp, hours=1))
27-
end_time: str = to_str(to_datetime(timestamp)) # built-ins handle CM API's formatting
35+
# Adjust time window to look at predictions that have had time to mature
36+
# Start: (evaluation_window + prediction) hours ago
37+
# End: prediction_future_hours ago (to ensure all predictions have matured)
38+
start_time: str = to_str(get_before(timestamp=timestamp, hours=evaluation_window_hours + prediction_future_hours))
39+
end_time: str = to_str(to_datetime(get_before(timestamp=timestamp, hours=prediction_future_hours)))
2840
# Query CM API for sample standard deviation of the 1s residuals
2941
historical_price_data: DataFrame = cm.get_CM_ReferenceRate(
3042
assets="BTC", start=start_time, end=end_time, frequency="1s"
3143
)
3244
cm_data = pd_to_dict(historical_price_data)
45+
3346
for uid, response in zip(self.available_uids, responses):
3447
current_miner = self.MinerHistory[uid]
3548
self.MinerHistory[uid].add_prediction(response.timestamp, response.prediction, response.interval)
36-
prediction_dict, interval_dict = current_miner.format_predictions(response.timestamp)
37-
mature_time_dict = mature_dictionary(prediction_dict)
49+
# Get predictions from the evaluation window that have had time to mature
50+
prediction_dict, interval_dict = current_miner.format_predictions(
51+
reference_timestamp=get_before(timestamp, hours=prediction_future_hours),
52+
hours=evaluation_window_hours,
53+
)
54+
55+
# Mature the predictions (shift forward by 1 hour)
56+
mature_time_dict = mature_dictionary(prediction_dict, hours=prediction_future_hours)
57+
3858
preds, price, aligned_pred_timestamps = align_timepoints(mature_time_dict, cm_data)
39-
for i, j, k in zip(preds, price, aligned_pred_timestamps):
40-
bt.logging.debug(f"Prediction: {i} | Price: {j} | Aligned Prediction: {k}")
59+
60+
num_predictions = len(preds) if preds is not None else 0
61+
62+
# Ensure a maximum ratio of 1.0
63+
completeness_ratio = min(num_predictions / expected_timepoints, 1.0)
64+
completeness_scores.append(completeness_ratio)
65+
bt.logging.debug(
66+
f"UID: {uid} | Completeness: {completeness_ratio:.2f} ({num_predictions}/{expected_timepoints})"
67+
)
68+
69+
# for i, j, k in zip(preds, price, aligned_pred_timestamps):
70+
# bt.logging.debug(f"Prediction: {i} | Price: {j} | Aligned Prediction: {k}")
4171
inters, interval_prices, aligned_int_timestamps = align_timepoints(interval_dict, cm_data)
42-
for i, j, k in zip(inters, interval_prices, aligned_int_timestamps):
43-
bt.logging.debug(f"Interval: {i} | Interval Price: {j} | Aligned TS: {k}")
44-
point_errors.append(point_error(preds, price))
72+
# for i, j, k in zip(inters, interval_prices, aligned_int_timestamps):
73+
# bt.logging.debug(f"Interval: {i} | Interval Price: {j} | Aligned TS: {k}")
74+
75+
# Penalize miners with missing predictions by increasing their point error
76+
if preds is None or len(preds) == 0:
77+
point_errors.append(np.inf) # Maximum penalty for no predictions
78+
else:
79+
# Calculate error as normal, but apply completeness penalty
80+
base_point_error = point_error(preds, price)
81+
# Apply penalty inversely proportional to completeness
82+
# This will increase error for incomplete prediction sets
83+
adjusted_point_error = base_point_error / completeness_ratio
84+
point_errors.append(adjusted_point_error)
85+
4586
if any([np.isnan(inters).any(), np.isnan(interval_prices).any()]):
4687
interval_errors.append(0)
4788
else:
48-
interval_errors.append(interval_error(inters, interval_prices))
89+
# Similarly, penalize interval errors for incompleteness
90+
base_interval_error = interval_error(inters, interval_prices)
91+
adjusted_interval_error = base_interval_error * completeness_ratio # Lower score for incomplete sets
92+
interval_errors.append(adjusted_interval_error)
93+
4994
bt.logging.debug(f"UID: {uid} | point_errors: {point_errors[-1]} | interval_errors: {interval_errors[-1]}")
5095

5196
point_ranks = rank(np.array(point_errors))
5297
interval_ranks = rank(-np.array(interval_errors)) # 1 is best, 0 is worst, so flip it
53-
rewards = (decayed_weights[point_ranks] + decayed_weights[interval_ranks]) / 2
98+
99+
base_rewards = (decayed_weights[point_ranks] + decayed_weights[interval_ranks]) / 2
100+
101+
# Simply multiply the final rewards by the completeness score
102+
rewards = base_rewards * np.array(completeness_scores)
103+
54104
return rewards
55105

56106

precog/validators/weight_setter.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from numpy import array
77
from pytz import timezone
88

9-
from precog import __spec_version__
9+
from precog import __spec_version__, constants
1010
from precog.protocol import Challenge
1111
from precog.utils.bittensor import check_uid_availability, print_info, setup_bittensor_objects
1212
from precog.utils.classes import MinerHistory
@@ -39,8 +39,7 @@ async def create(cls, config=None, loop=None):
3939
async def initialize(self):
4040
setup_bittensor_objects(self)
4141
self.timezone = timezone("UTC")
42-
self.prediction_interval = self.config.prediction_interval # in seconds
43-
self.N_TIMEPOINTS = self.config.N_TIMEPOINTS # number of timepoints to predict
42+
self.prediction_interval = constants.PREDICTION_INTERVAL_MINUTES
4443
self.hyperparameters = func_with_retry(self.subtensor.get_subnet_hyperparameters, netuid=self.config.netuid)
4544
self.resync_metagraph_rate = 600 # in seconds
4645
bt.logging.info(

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "precog"
3-
version = "2.2.2"
3+
version = "2.3.0"
44
description = "Bitcoin Price Prediction Subnet"
55
authors = ["Coin Metrics", "Yuma Group"]
66
readme = "README.md"

tests/test_package.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ def setUp(self):
1616
def test_package_version(self):
1717
# Check that version is as expected
1818
# Must update to increment package version successfully
19-
self.assertEqual(__version__, "2.2.2")
19+
self.assertEqual(__version__, "2.3.0")

0 commit comments

Comments
 (0)