Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ version: 2

# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "3.10"
python: "3.12"
jobs:
pre_build:
- sphinx-apidoc --separate --no-toc --force -o docs/api/ numerapi
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
# Changelog
Notable changes to this project.

## [2.24.0] - 2026-08-03
- add exact `roundScoreConfigs` identities, scoring windows, and payout settings
to `list_rounds` for Classic, Signals, and Crypto
- stop querying deprecated GraphQL round multiplier fields; keep the six
established Corr/MMC return keys as exact-name compatibility projections
until their scheduled removal in numerapi 3.0.0
- document migration from legacy round multiplier roles and isolate the
deprecated `round_model_performances_v2` behavior

## [2.23.3] - 2026-06-30
- fix `models_of_account` referencing incorrect type `Str!` instead of `String!`

## [2.23.2] - 2026-06-02
- increase dataset download chunk size to 1 MB to improve download speeds

## [2.23.1] - 2026-04-23
- fix package version lookup
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
'sphinx.ext.doctest',
'm2r'
'sphinx_mdinclude',
]

# Add any paths that contain templates here, relative to this directory.
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Contents
:maxdepth: 2

changelog
round-score-configs
license

Indices and tables
Expand Down
7 changes: 6 additions & 1 deletion docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
m2r
sphinx-mdinclude
python-dateutil
tqdm
pandas
click
pytz
92 changes: 92 additions & 0 deletions docs/round-score-configs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Round score and payout configuration

Starting in numerapi 2.24.0, `NumerAPI.list_rounds()`,
`SignalsAPI.list_rounds()`, and `CryptoAPI.list_rounds()` return the public
`roundScoreConfigs` list. Each item is an exact score definition and per-round
snapshot from the Tournament API. New code should select entries by `name`,
`version`, or `scoreConfigId`; it should not infer score identity from a legacy
payout role.

Each item includes:

- identity: `id`, `scoreConfigId`, `name`, `version`, and `displayName`;
- applicability: `roundNumberStart`, `roundNumberEnd`, `universe`,
and `isCanonScore`;
- scoring: `totalScoreDays`, `returnsLagDays`, `dataDelayDays`,
`scoringStart`, and `scoringEnd`;
- payout settings: `isPayout`, `minMultiplier`, `maxMultiplier`,
`defaultMultiplier`, `clipThreshold`, `stakeThreshold`, and `payoutFactor`.

`scoringStart` and `scoringEnd` are returned as `datetime.datetime` objects,
consistent with other date fields in numerapi. GraphQL float and integer fields
retain their normal Python JSON types.

## Migrating from legacy multiplier keys

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
were Alpha and MPC:

```python
{
"defaultCorrMultiplier": 0.3,
"defaultMmcMultiplier": 0.8,
}
```

In 2.24.0 the exact identities are available without knowing score names in
advance:

```python
{
"roundScoreConfigs": [
{
"scoreConfigId": "...",
"name": "alpha",
"version": "2",
"displayName": "alpha",
"isPayout": True,
"defaultMultiplier": 0.3,
# Other identity, scoring, timing, and payout fields omitted.
},
{
"scoreConfigId": "...",
"name": "meta_portfolio_contribution",
"version": "2",
"displayName": "mpc",
"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`.

## Deprecated performance endpoint

`round_model_performances_v2()` remains an isolated deprecated compatibility
method. Its `corrMultiplier` and `mmcMultiplier` fields come from the deprecated
`v2RoundModelPerformances` GraphQL endpoint and must not be used to infer score
identity. Use `submission_scores()` for identity-preserving score results and
join them to `list_rounds()` by round when payout configuration is needed.
Neither performance method nor `list_rounds()` has a dedicated CLI command, so
there is no CLI return shape to migrate.
98 changes: 90 additions & 8 deletions numerapi/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def models_of_account(self, account) -> Dict[str, str]:
{'uuazed': '9b157d9b-ce61-4ab5-9413-413f13a0c0a6', ...}
"""
query = """
query($username: Str!
query($username: String!
$tournament: Int) {
accountProfile(username: $username
tournament: $tournament){
Expand Down Expand Up @@ -637,7 +637,16 @@ def list_rounds(
limit (int, optional): maximum number of rounds to return

Returns:
list of dicts: round entries matching the provided filters
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
Expand All @@ -663,13 +672,30 @@ def list_rounds(
resolvedStaking
payoutFactor
stakeThreshold
minCorrMultiplier
maxCorrMultiplier
defaultCorrMultiplier
minMmcMultiplier
maxMmcMultiplier
defaultMmcMultiplier
dataDatestamp
roundScoreConfigs {
id
scoreConfigId
roundNumberStart
roundNumberEnd
name
version
displayName
totalScoreDays
returnsLagDays
dataDelayDays
universe
isCanonScore
isPayout
scoringStart
scoringEnd
minMultiplier
maxMultiplier
defaultMultiplier
clipThreshold
stakeThreshold
payoutFactor
}
}
}
"""
Expand All @@ -691,8 +717,64 @@ def list_rounds(
]:
utils.replace(round_info, field, utils.parse_datetime_string)
utils.replace(round_info, "payoutFactor", utils.parse_float_string)
for config in round_info["roundScoreConfigs"]:
utils.replace(
config, "scoringStart", utils.parse_datetime_string
)
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.

Expand Down
6 changes: 4 additions & 2 deletions numerapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

logger = logging.getLogger(__name__)

DOWNLOAD_CHUNK_SIZE = 1024 * 1024 # 1 MiB


def load_secrets() -> tuple:
"""load secrets from environment variables or dotenv file"""
Expand Down Expand Up @@ -96,9 +98,9 @@ def download_file(url: str, dest_path: str, show_progress_bars: bool = True):
# Update progress bar to reflect how much of the file is already downloaded
pbar.update(file_size)
with open(temp_path, "ab") as dest_file:
for chunk in req.iter_content(1024):
for chunk in req.iter_content(DOWNLOAD_CHUNK_SIZE):
dest_file.write(chunk)
pbar.update(1024)
pbar.update(len(chunk))
# move temp file to target destination
os.replace(temp_path, dest_path)
return dest_path
Expand Down
4 changes: 4 additions & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[lint]
# Keep the repository's historical lint baseline stable while the CI action
# follows unpinned Ruff releases.
select = ["E4", "E7", "E9", "F"]
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def load(path):
return open(path, "r").read()


numerapi_version = "2.23.1"
numerapi_version = "2.24.0"


classifiers = [
Expand Down
Loading
Loading