From 5f9e069409526e9125692c0da7b0a5fb98362898 Mon Sep 17 00:00:00 2001 From: "numerai-hosted-agent-bot[bot]" <247676148+numerai-hosted-agent-bot[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:17:47 -0700 Subject: [PATCH] NumerAPI 3.0: payout multiplier removal (#134) --- CHANGELOG.md | 4 ++ docs/round-score-configs.md | 33 ++++--------- numerapi/base_api.py | 56 --------------------- setup.py | 2 +- tests/test_base_api.py | 97 +++++++------------------------------ 5 files changed, 33 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e643e0d..e640445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog Notable changes to this project. +## [3.0.0] - Unreleased +- remove the six deprecated Corr/MMC multiplier projections from + `list_rounds`; use the identity-preserving `roundScoreConfigs` list instead + ## [2.24.0] - 2026-08-03 - add exact `roundScoreConfigs` identities, scoring windows, and payout settings to `list_rounds` for Classic, Signals, and Crypto diff --git a/docs/round-score-configs.md b/docs/round-score-configs.md index 99db25d..bac0613 100644 --- a/docs/round-score-configs.md +++ b/docs/round-score-configs.md @@ -21,7 +21,7 @@ Each item includes: consistent with other date fields in numerapi. GraphQL float and integer fields retain their normal Python JSON types. -## Migrating from legacy multiplier keys +## Legacy multiplier keys removed in 3.0.0 Before 2.24.0, `list_rounds()` requested server compatibility fields. For a Signals round, a response could look like this even though the payout scores @@ -34,8 +34,8 @@ were Alpha and MPC: } ``` -In 2.24.0 the exact identities are available without knowing score names in -advance: +In 3.0.0, the exact identities are the only payout configuration returned by +`list_rounds()`: ```python { @@ -57,29 +57,16 @@ advance: "isPayout": True, "defaultMultiplier": 0.8, }, - ], - "defaultCorrMultiplier": None, - "defaultMmcMultiplier": None, + ] } ``` -The six established Corr/MMC keys (`min`, `max`, and `default` for each) stay -in the returned round dictionary throughout numerapi 2.x. They are now -identity-safe projections: Corr keys select only a payout config whose `name` -is exactly `correlation`, MMC keys select only a payout config whose `name` is -exactly `meta_model_contribution`, and the keys are `None` when there is no -exact match. Alpha and FNC are never projected as Corr; MPC is never projected -as MMC. If multiple exact payout configs exist, the projection uses the config -with the newest `roundNumberStart`, then compares the numeric `version` values -as integers and uses `id` for a numeric-version tie. If multiple configs at the -newest start contain a non-numeric future version, the compatibility keys are -`None` rather than guessing an order. The complete list remains available -unchanged in either case. - -These six compatibility keys are scheduled for removal in numerapi 3.0.0. -`list_rounds()` never exposed the three legacy TC multiplier fields, so this -migration does not introduce them. Code should migrate now by filtering -`roundScoreConfigs`, normally starting with `isPayout`. +The six Corr/MMC compatibility keys (`min`, `max`, and `default` for each) are +no longer added to the returned round dictionary. `list_rounds()` never +exposed the three legacy TC multiplier fields. Code should filter +`roundScoreConfigs`, normally starting with `isPayout`, and preserve each +configuration's `name`, `version`, and `scoreConfigId` rather than projecting +different scores into Corr or MMC roles. ## Deprecated performance endpoint diff --git a/numerapi/base_api.py b/numerapi/base_api.py index 8d332c4..afde839 100644 --- a/numerapi/base_api.py +++ b/numerapi/base_api.py @@ -751,13 +751,6 @@ def list_rounds( list of dicts: round entries matching the provided filters. Each entry includes ``roundScoreConfigs``, whose items retain the exact score identity and per-round payout settings returned by the API. - - The legacy ``minCorrMultiplier`` through - ``defaultMmcMultiplier`` keys remain until numerapi 3.0.0. They are - compatibility projections of payout configs whose names are - exactly ``correlation`` or ``meta_model_contribution``; they are - ``None`` when no such payout config exists. Use - ``roundScoreConfigs`` for all new integrations. """ query = """ query($tournament: Int @@ -835,57 +828,8 @@ def list_rounds( utils.replace( config, "scoringEnd", utils.parse_datetime_string ) - self._add_legacy_round_multipliers(round_info) return rounds - @staticmethod - def _add_legacy_round_multipliers(round_info: dict) -> None: - """Add deprecated, identity-safe round multiplier projections.""" - legacy_scores = { - "Corr": "correlation", - "Mmc": "meta_model_contribution", - } - multiplier_fields = { - "min": "minMultiplier", - "max": "maxMultiplier", - "default": "defaultMultiplier", - } - - for legacy_name, score_name in legacy_scores.items(): - matches = [ - config - for config in round_info["roundScoreConfigs"] - if config["isPayout"] and config["name"] == score_name - ] - config = Api._select_legacy_round_config(matches) - for prefix, config_field in multiplier_fields.items(): - field = f"{prefix}{legacy_name}Multiplier" - round_info[field] = ( - None if config is None else config[config_field] - ) - - @staticmethod - def _select_legacy_round_config(configs: List[Dict]) -> Dict | None: - """Select the latest config, failing closed on ambiguous versions.""" - if not configs: - return None - - latest_start = max(item["roundNumberStart"] for item in configs) - candidates = [ - item for item in configs if item["roundNumberStart"] == latest_start - ] - if len(candidates) == 1: - return candidates[0] - - try: - return max( - candidates, - key=lambda item: (int(item["version"]), item["id"]), - ) - except (TypeError, ValueError): - # A future non-numeric version contract cannot be ordered safely. - return None - def set_bio(self, model_id: str, bio: str) -> bool: """Set bio field for a model id. diff --git a/setup.py b/setup.py index 1d93053..2e25bd8 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ def load(path): return open(path, "r").read() -numerapi_version = "2.24.0" +numerapi_version = "3.0.0" classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/tests/test_base_api.py b/tests/test_base_api.py index 7e832f1..d10622a 100644 --- a/tests/test_base_api.py +++ b/tests/test_base_api.py @@ -369,8 +369,14 @@ def test_list_rounds(api): res[0]["roundScoreConfigs"][0]["scoringEnd"], datetime.datetime ) assert res[0]["roundScoreConfigs"][0]["scoreConfigId"] == "classic-corr" - assert res[0]["defaultCorrMultiplier"] == 0.75 - assert res[0]["defaultMmcMultiplier"] == 2.25 + assert not { + "minCorrMultiplier", + "maxCorrMultiplier", + "defaultCorrMultiplier", + "minMmcMultiplier", + "maxMmcMultiplier", + "defaultMmcMultiplier", + }.intersection(res[0]) request_body = json.loads(responses.calls[0].request.body) assert request_body["variables"]["tournament"] == 8 @@ -403,18 +409,23 @@ def test_list_rounds(api): } assert "roundScoreConfigs" in request_body["query"] assert all(field in request_body["query"] for field in requested_fields) - assert "minCorrMultiplier" not in request_body["query"] - assert "minMmcMultiplier" not in request_body["query"] + assert not { + "minCorrMultiplier", + "maxCorrMultiplier", + "defaultCorrMultiplier", + "minMmcMultiplier", + "maxMmcMultiplier", + "defaultMmcMultiplier", + }.intersection(request_body["query"].split()) @pytest.mark.parametrize( - ("api_class", "tournament", "score_names", "legacy_multipliers"), + ("api_class", "tournament", "score_names"), [ ( numerapi.NumerAPI, 8, ["correlation", "meta_model_contribution"], - (0.5, 0.5), ), ( numerapi.SignalsAPI, @@ -424,19 +435,17 @@ def test_list_rounds(api): "v4_feature_neutral_correlation", "meta_portfolio_contribution", ], - (None, None), ), ( numerapi.CryptoAPI, 12, ["correlation", "meta_model_contribution"], - (0.5, 0.5), ), ], ) @responses.activate def test_list_rounds_preserves_tournament_score_identities( - api_class, tournament, score_names, legacy_multipliers + api_class, tournament, score_names ): api = api_class() configs = [_round_score_config(name) for name in score_names] @@ -452,8 +461,6 @@ def test_list_rounds_preserves_tournament_score_identities( assert [ config["name"] for config in returned_round["roundScoreConfigs"] ] == score_names - assert returned_round["defaultCorrMultiplier"] == legacy_multipliers[0] - assert returned_round["defaultMmcMultiplier"] == legacy_multipliers[1] request_body = json.loads(responses.calls[0].request.body) assert request_body["variables"]["tournament"] == tournament @@ -492,74 +499,6 @@ def test_list_rounds_keeps_coexisting_and_unfamiliar_score_configs(api): config["scoreConfigId"] for config in returned_round["roundScoreConfigs"] ] == [config["scoreConfigId"] for config in configs] - assert returned_round["defaultCorrMultiplier"] == 0.4 - assert returned_round["defaultMmcMultiplier"] == 0.6 - - -@responses.activate -def test_list_rounds_orders_numeric_score_versions_numerically(api): - configs = [ - _round_score_config( - "correlation", - config_id="corr-9", - version="9", - multiplier=0.9, - round_number_start=200, - ), - _round_score_config( - "correlation", - config_id="corr-10", - version="10", - multiplier=1.0, - round_number_start=200, - ), - ] - responses.add( - responses.POST, - base_api.API_TOURNAMENT_URL, - json={"data": {"rounds": [{"roundScoreConfigs": configs}]}}, - ) - - returned_round = api.list_rounds()[0] - - assert [ - returned_round["minCorrMultiplier"], - returned_round["maxCorrMultiplier"], - returned_round["defaultCorrMultiplier"], - ] == [1.0, 1.0, 1.0] - - -@responses.activate -def test_list_rounds_fails_closed_for_ambiguous_non_numeric_versions(api): - configs = [ - _round_score_config( - "correlation", - config_id="corr-10", - version="10", - multiplier=1.0, - round_number_start=200, - ), - _round_score_config( - "correlation", - config_id="corr-next", - version="next", - multiplier=1.1, - round_number_start=200, - ), - ] - responses.add( - responses.POST, - base_api.API_TOURNAMENT_URL, - json={"data": {"rounds": [{"roundScoreConfigs": configs}]}}, - ) - - returned_round = api.list_rounds()[0] - - assert [ - returned_round["minCorrMultiplier"], - returned_round["maxCorrMultiplier"], - returned_round["defaultCorrMultiplier"], - ] == [None, None, None] @responses.activate