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.
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/PKG-INFO +1 -1
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/base_api.py +145 -1
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/PKG-INFO +1 -1
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/setup.py +1 -1
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_base_api.py +157 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_numerapi.py +4 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_signalsapi.py +1 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/LICENSE +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/README.md +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/__init__.py +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/cli.py +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/cryptoapi.py +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/numerapi.py +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/py.typed +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/signalsapi.py +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi/utils.py +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/SOURCES.txt +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/dependency_links.txt +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/entry_points.txt +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/requires.txt +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/numerapi.egg-info/top_level.txt +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/setup.cfg +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_cli.py +0 -0
- {numerapi-2.23.0.dev0 → numerapi-2.23.0.dev2}/tests/test_utils.py +0 -0
|
@@ -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
|
-
|
|
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,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):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|