numerapi 2.23.0.dev0__tar.gz → 2.23.0.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/PKG-INFO +1 -1
  2. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/base_api.py +145 -1
  3. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/PKG-INFO +1 -1
  4. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/setup.py +1 -1
  5. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_base_api.py +157 -0
  6. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_numerapi.py +4 -0
  7. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_signalsapi.py +1 -0
  8. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/LICENSE +0 -0
  9. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/README.md +0 -0
  10. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/__init__.py +0 -0
  11. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/cli.py +0 -0
  12. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/cryptoapi.py +0 -0
  13. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/numerapi.py +0 -0
  14. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/py.typed +0 -0
  15. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/signalsapi.py +0 -0
  16. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/utils.py +0 -0
  17. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/SOURCES.txt +0 -0
  18. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/dependency_links.txt +0 -0
  19. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/entry_points.txt +0 -0
  20. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/requires.txt +0 -0
  21. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/top_level.txt +0 -0
  22. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/setup.cfg +0 -0
  23. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_cli.py +0 -0
  24. {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numerapi
3
- Version: 2.23.0.dev0
3
+ Version: 2.23.0.dev2
4
4
  Summary: Automatically download and upload data for the Numerai machine learning competition
5
5
  Home-page: https://github.com/uuazed/numerapi
6
6
  Maintainer: uuazed
@@ -3,6 +3,7 @@
3
3
  import datetime
4
4
  import logging
5
5
  import os
6
+ import warnings
6
7
  from io import BytesIO
7
8
  from typing import Dict, List, Tuple, Union
8
9
 
@@ -426,7 +427,7 @@ class Api:
426
427
 
427
428
  max_amount = max_amount if max_amount is not None else amount
428
429
  query = """
429
- mutation($submissionId: ID!, $staker: String!, $maxAmount: String!) {
430
+ query($submissionId: ID!, $staker: String!, $maxAmount: String!) {
430
431
  v3StakeAuth(
431
432
  submissionId: $submissionId
432
433
  staker: $staker
@@ -1046,9 +1047,145 @@ class Api:
1046
1047
  utils.replace(results, "updatedAt", utils.parse_datetime_string)
1047
1048
  return results
1048
1049
 
1050
+ def submission_scores(
1051
+ self,
1052
+ model_id: str,
1053
+ display_name: str | None = None,
1054
+ version: str | None = None,
1055
+ day: int | None = None,
1056
+ resolved: bool | None = None,
1057
+ tournament: int | None = None,
1058
+ last_n_rounds: int | None = None,
1059
+ distinct_on_round: bool | None = None,
1060
+ ) -> List[Dict]:
1061
+ """Fetch submission score history for a model.
1062
+
1063
+ Args:
1064
+ model_id (str): target model UUID
1065
+ display_name (str, optional): score metric name filter
1066
+ version (str, optional): score version filter
1067
+ day (int, optional): day filter
1068
+ resolved (bool, optional): resolved-state filter
1069
+ tournament (int, optional): tournament filter, defaults to the
1070
+ API instance tournament
1071
+ last_n_rounds (int, optional): limit by most recent rounds
1072
+ distinct_on_round (bool, optional): keep only the latest score per
1073
+ round after applying other filters
1074
+
1075
+ Returns:
1076
+ list of dicts: list of submission score entries
1077
+ """
1078
+
1079
+ query = """
1080
+ query($modelId: ID!
1081
+ $displayName: String
1082
+ $version: String
1083
+ $day: Int
1084
+ $resolved: Boolean
1085
+ $tournament: Int
1086
+ $lastNRounds: Int
1087
+ $distinctOnRound: Boolean) {
1088
+ submissionScores(modelId: $modelId
1089
+ displayName: $displayName
1090
+ version: $version
1091
+ day: $day
1092
+ resolved: $resolved
1093
+ tournament: $tournament
1094
+ lastNRounds: $lastNRounds
1095
+ distinctOnRound: $distinctOnRound) {
1096
+ roundId
1097
+ submissionId
1098
+ roundNumber
1099
+ roundResolveTime
1100
+ roundScoreTime
1101
+ roundCloseStakingTime
1102
+ value
1103
+ percentile
1104
+ displayName
1105
+ version
1106
+ date
1107
+ day
1108
+ resolveDate
1109
+ resolved
1110
+ }
1111
+ }
1112
+ """
1113
+ arguments = {
1114
+ "modelId": model_id,
1115
+ "displayName": display_name,
1116
+ "version": version,
1117
+ "day": day,
1118
+ "resolved": resolved,
1119
+ "tournament": self.tournament_id if tournament is None else tournament,
1120
+ "lastNRounds": last_n_rounds,
1121
+ "distinctOnRound": distinct_on_round,
1122
+ }
1123
+ scores = self.raw_query(query, arguments)["data"]["submissionScores"]
1124
+ for score in scores:
1125
+ utils.replace(score, "roundResolveTime", utils.parse_datetime_string)
1126
+ utils.replace(score, "roundScoreTime", utils.parse_datetime_string)
1127
+ utils.replace(score, "roundCloseStakingTime", utils.parse_datetime_string)
1128
+ utils.replace(score, "date", utils.parse_datetime_string)
1129
+ utils.replace(score, "resolveDate", utils.parse_datetime_string)
1130
+ return scores
1131
+
1132
+ def pending_model_payouts(self, tournament: int | None = None) -> Dict:
1133
+ """Fetch actual and pending payouts for the authenticated user's models.
1134
+
1135
+ Args:
1136
+ tournament (int, optional): tournament filter, defaults to the API
1137
+ instance tournament
1138
+
1139
+ Returns:
1140
+ dict: payout groups with `actual` and `pending` lists
1141
+ """
1142
+
1143
+ query = """
1144
+ query($tournament: Int!) {
1145
+ pendingModelPayouts(tournament: $tournament) {
1146
+ actual {
1147
+ roundId
1148
+ roundNumber
1149
+ roundResolveTime
1150
+ modelId
1151
+ modelName
1152
+ modelDisplayName
1153
+ payoutNmr
1154
+ payoutValue
1155
+ currencySymbol
1156
+ }
1157
+ pending {
1158
+ roundId
1159
+ roundNumber
1160
+ roundResolveTime
1161
+ modelId
1162
+ modelName
1163
+ modelDisplayName
1164
+ payoutNmr
1165
+ payoutValue
1166
+ currencySymbol
1167
+ }
1168
+ }
1169
+ }
1170
+ """
1171
+ arguments = {
1172
+ "tournament": self.tournament_id if tournament is None else tournament
1173
+ }
1174
+ payouts = self.raw_query(query, arguments, authorization=True)["data"][
1175
+ "pendingModelPayouts"
1176
+ ]
1177
+ for payout_type in ["actual", "pending"]:
1178
+ for payout in payouts[payout_type]:
1179
+ utils.replace(payout, "roundResolveTime", utils.parse_datetime_string)
1180
+ utils.replace(payout, "payoutNmr", utils.parse_float_string)
1181
+ utils.replace(payout, "payoutValue", utils.parse_float_string)
1182
+ return payouts
1183
+
1049
1184
  def round_model_performances_v2(self, model_id: str):
1050
1185
  """Fetch round model performance of a user.
1051
1186
 
1187
+ DEPRECATED - please use `submission_scores` instead when possible.
1188
+
1052
1189
  Args:
1053
1190
  model_id (str)
1054
1191
 
@@ -1076,6 +1213,13 @@ class Api:
1076
1213
  * percentile (`float`)
1077
1214
  * value (`float`): value of the metric
1078
1215
  """
1216
+ warnings.warn(
1217
+ "`round_model_performances_v2` is deprecated because it relies on "
1218
+ "`v2RoundModelPerformances`. Use `submission_scores` instead when "
1219
+ "possible.",
1220
+ DeprecationWarning,
1221
+ stacklevel=2,
1222
+ )
1079
1223
 
1080
1224
  query = """
1081
1225
  query($modelId: String!
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numerapi
3
- Version: 2.23.0.dev0
3
+ Version: 2.23.0.dev2
4
4
  Summary: Automatically download and upload data for the Numerai machine learning competition
5
5
  Home-page: https://github.com/uuazed/numerapi
6
6
  Maintainer: uuazed
@@ -5,7 +5,7 @@ def load(path):
5
5
  return open(path, "r").read()
6
6
 
7
7
 
8
- numerapi_version = "2.23.0.dev0"
8
+ numerapi_version = "2.23.0.dev2"
9
9
 
10
10
 
11
11
  classifiers = [
@@ -1,3 +1,5 @@
1
+ import datetime
2
+ import decimal
1
3
  import json
2
4
  import os
3
5
  import pytest
@@ -35,8 +37,14 @@ def test__login(api):
35
37
  assert api.token == ("id", "key")
36
38
 
37
39
 
40
+ @responses.activate
38
41
  def test_raw_query(api):
39
42
  query = "query {latestNmrPrice {priceUsd}}"
43
+ responses.add(
44
+ responses.POST,
45
+ base_api.API_TOURNAMENT_URL,
46
+ json={"data": {"latestNmrPrice": {"priceUsd": "42.00"}}},
47
+ )
40
48
  result = api.raw_query(query)
41
49
  assert isinstance(result, dict)
42
50
  assert "data" in result
@@ -119,6 +127,151 @@ def test_set_submission_webhook(api):
119
127
  assert res
120
128
 
121
129
 
130
+ @responses.activate
131
+ def test_submission_scores(api):
132
+ api.tournament_id = 11
133
+ data = {
134
+ "data": {
135
+ "submissionScores": [
136
+ {
137
+ "roundId": "round-1",
138
+ "submissionId": "submission-1",
139
+ "roundNumber": 123,
140
+ "roundResolveTime": "2026-04-01T00:00:00Z",
141
+ "roundScoreTime": "2026-03-29T00:00:00Z",
142
+ "roundCloseStakingTime": "2026-03-28T00:00:00Z",
143
+ "value": 0.12,
144
+ "percentile": 0.95,
145
+ "displayName": "CORR20",
146
+ "version": "v5",
147
+ "date": "2026-03-30T00:00:00Z",
148
+ "day": 2,
149
+ "resolveDate": "2026-04-01T00:00:00Z",
150
+ "resolved": True,
151
+ }
152
+ ]
153
+ }
154
+ }
155
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
156
+
157
+ res = api.submission_scores(
158
+ "model-1",
159
+ display_name="CORR20",
160
+ version="v5",
161
+ day=2,
162
+ resolved=True,
163
+ last_n_rounds=5,
164
+ distinct_on_round=True,
165
+ )
166
+
167
+ assert len(res) == 1
168
+ assert res[0]["displayName"] == "CORR20"
169
+ assert isinstance(res[0]["roundResolveTime"], datetime.datetime)
170
+ assert isinstance(res[0]["roundScoreTime"], datetime.datetime)
171
+ assert isinstance(res[0]["roundCloseStakingTime"], datetime.datetime)
172
+ assert isinstance(res[0]["date"], datetime.datetime)
173
+ assert isinstance(res[0]["resolveDate"], datetime.datetime)
174
+
175
+ request_body = json.loads(responses.calls[0].request.body)
176
+ assert request_body["variables"]["tournament"] == 11
177
+ assert request_body["variables"]["lastNRounds"] == 5
178
+ assert request_body["variables"]["distinctOnRound"] is True
179
+
180
+
181
+ @responses.activate
182
+ def test_pending_model_payouts(api):
183
+ api.token = ("", "")
184
+ api.tournament_id = 12
185
+ data = {
186
+ "data": {
187
+ "pendingModelPayouts": {
188
+ "actual": [
189
+ {
190
+ "roundId": "round-a",
191
+ "roundNumber": 12,
192
+ "roundResolveTime": "2026-04-02T00:00:00Z",
193
+ "modelId": "model-a",
194
+ "modelName": "alpha",
195
+ "modelDisplayName": "Alpha",
196
+ "payoutNmr": "2.5000",
197
+ "payoutValue": "31.20",
198
+ "currencySymbol": "$",
199
+ }
200
+ ],
201
+ "pending": [
202
+ {
203
+ "roundId": "round-b",
204
+ "roundNumber": 13,
205
+ "roundResolveTime": "2026-04-09T00:00:00Z",
206
+ "modelId": "model-b",
207
+ "modelName": "beta",
208
+ "modelDisplayName": "Beta",
209
+ "payoutNmr": "1.1000",
210
+ "payoutValue": "13.73",
211
+ "currencySymbol": "$",
212
+ }
213
+ ],
214
+ }
215
+ }
216
+ }
217
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
218
+
219
+ res = api.pending_model_payouts()
220
+
221
+ assert len(res["actual"]) == 1
222
+ assert len(res["pending"]) == 1
223
+ assert res["actual"][0]["payoutNmr"] == decimal.Decimal("2.5000")
224
+ assert res["pending"][0]["payoutValue"] == decimal.Decimal("13.73")
225
+ assert isinstance(res["actual"][0]["roundResolveTime"], datetime.datetime)
226
+ assert isinstance(res["pending"][0]["roundResolveTime"], datetime.datetime)
227
+
228
+ request_body = json.loads(responses.calls[0].request.body)
229
+ assert request_body["variables"]["tournament"] == 12
230
+
231
+
232
+ @responses.activate
233
+ def test_round_model_performances_v2_warns(api):
234
+ api.tournament_id = 8
235
+ data = {
236
+ "data": {
237
+ "v2RoundModelPerformances": [
238
+ {
239
+ "atRisk": "10.5",
240
+ "corrMultiplier": 1.0,
241
+ "mmcMultiplier": 0.5,
242
+ "roundPayoutFactor": "0.8",
243
+ "roundNumber": 456,
244
+ "roundOpenTime": "2026-03-01T00:00:00Z",
245
+ "roundResolveTime": "2026-03-29T00:00:00Z",
246
+ "roundResolved": True,
247
+ "roundTarget": "main",
248
+ "submissionScores": [
249
+ {
250
+ "date": "2026-03-28T00:00:00Z",
251
+ "day": 20,
252
+ "displayName": "CORR20",
253
+ "payoutPending": "0.8",
254
+ "payoutSettled": "0.7",
255
+ "percentile": 0.9,
256
+ "value": 0.12,
257
+ }
258
+ ],
259
+ }
260
+ ]
261
+ }
262
+ }
263
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
264
+
265
+ with pytest.warns(DeprecationWarning, match="round_model_performances_v2"):
266
+ res = api.round_model_performances_v2("model-1")
267
+
268
+ assert len(res) == 1
269
+ assert res[0]["atRisk"] == decimal.Decimal("10.5")
270
+ assert isinstance(res[0]["roundOpenTime"], datetime.datetime)
271
+ assert isinstance(res[0]["roundResolveTime"], datetime.datetime)
272
+ assert res[0]["submissionScores"][0]["payoutPending"] == decimal.Decimal("0.8")
273
+
274
+
122
275
  @responses.activate
123
276
  def test_v3_stake_auth(api):
124
277
  api.token = ("", "")
@@ -144,6 +297,8 @@ def test_v3_stake_auth(api):
144
297
  result = api.v3_stake_auth("submission-id", "0xstaker", amount=25)
145
298
 
146
299
  body = json.loads(responses.calls[0].request.body)
300
+ assert "mutation" not in body["query"]
301
+ assert "query(" in body["query"]
147
302
  assert "v3StakeAuth" in body["query"]
148
303
  assert body["variables"]["submissionId"] == "submission-id"
149
304
  assert body["variables"]["maxAmount"] == "25"
@@ -182,6 +337,8 @@ def test_v3_stake_auth_accepts_max_amount(api):
182
337
  )
183
338
 
184
339
  body = json.loads(responses.calls[0].request.body)
340
+ assert "mutation" not in body["query"]
341
+ assert "query(" in body["query"]
185
342
  assert body["variables"]["maxAmount"] == "30"
186
343
  assert result["maxAmount"] == "30"
187
344
  assert result["amount"] == "30"
@@ -16,17 +16,20 @@ def api_fixture():
16
16
  return api
17
17
 
18
18
 
19
+ @pytest.mark.live_api
19
20
  def test_get_competitions(api):
20
21
  res = api.get_competitions(tournament=1)
21
22
  assert isinstance(res, list)
22
23
  assert len(res) > 80
23
24
 
24
25
 
26
+ @pytest.mark.live_api
25
27
  def test_get_current_round(api):
26
28
  current_round = api.get_current_round()
27
29
  assert current_round >= 82
28
30
 
29
31
 
32
+ @pytest.mark.live_api
30
33
  @pytest.mark.parametrize("fun", ["get_account", "wallet_transactions"])
31
34
  def test_unauthorized_requests(api, fun):
32
35
  with pytest.raises(ValueError) as err:
@@ -38,6 +41,7 @@ def test_unauthorized_requests(api, fun):
38
41
  "Your session is invalid or has expired." in str(err.value)
39
42
 
40
43
 
44
+ @pytest.mark.live_api
41
45
  def test_error_handling(api):
42
46
  # String instead of Int
43
47
  with pytest.raises(ValueError):
@@ -13,6 +13,7 @@ def api_fixture():
13
13
  return api
14
14
 
15
15
 
16
+ @pytest.mark.live_api
16
17
  def test_get_leaderboard(api):
17
18
  lb = api.get_leaderboard(1)
18
19
  assert len(lb) == 1
File without changes
File without changes
File without changes