numerapi 2.22.0__tar.gz → 2.23.0.dev1__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 (25) hide show
  1. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/PKG-INFO +1 -1
  2. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/base_api.py +350 -1
  3. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi.egg-info/PKG-INFO +1 -1
  4. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/setup.py +1 -1
  5. numerapi-2.23.0.dev1/tests/test_base_api.py +417 -0
  6. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/tests/test_numerapi.py +6 -0
  7. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/tests/test_signalsapi.py +1 -0
  8. numerapi-2.22.0/tests/test_base_api.py +0 -118
  9. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/LICENSE +0 -0
  10. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/README.md +0 -0
  11. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/__init__.py +0 -0
  12. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/cli.py +0 -0
  13. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/cryptoapi.py +0 -0
  14. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/numerapi.py +0 -0
  15. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/py.typed +0 -0
  16. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/signalsapi.py +0 -0
  17. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi/utils.py +0 -0
  18. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi.egg-info/SOURCES.txt +0 -0
  19. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi.egg-info/dependency_links.txt +0 -0
  20. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi.egg-info/entry_points.txt +0 -0
  21. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi.egg-info/requires.txt +0 -0
  22. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/numerapi.egg-info/top_level.txt +0 -0
  23. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/setup.cfg +0 -0
  24. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/tests/test_cli.py +0 -0
  25. {numerapi-2.22.0 → numerapi-2.23.0.dev1}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numerapi
3
- Version: 2.22.0
3
+ Version: 2.23.0.dev1
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
 
@@ -383,6 +384,211 @@ class Api:
383
384
  }
384
385
  return mapping
385
386
 
387
+ def v3_stake_auth(
388
+ self,
389
+ submission_id: str,
390
+ staker: str,
391
+ amount: float | str | None = None,
392
+ max_amount: float | str | None = None,
393
+ ) -> Dict:
394
+ """Issue a staking v3 authorization for a selected submission.
395
+
396
+ Args:
397
+ submission_id (str): submission id for the selected submission
398
+ staker (str): staker wallet address
399
+ amount (float or str, optional): max stake amount for the
400
+ authorization. Retained as a backwards-compatible alias for
401
+ `max_amount`.
402
+ max_amount (float or str, optional): max stake amount for the
403
+ authorization.
404
+
405
+ Returns:
406
+ dict: authorization payload with the following fields:
407
+
408
+ * authorizationSigner (`str`)
409
+ * authorizationDigest (`str`)
410
+ * chainId (`str`)
411
+ * deadline (`str`)
412
+ * maxAmount (`str`)
413
+ * amount (`str`) alias for `maxAmount`
414
+ * modelId (`str`)
415
+ * nmrAddress (`str`)
416
+ * nonce (`str`)
417
+ * roundId (`str`)
418
+ * signature (`str`)
419
+ * staker (`str`)
420
+ * stakingAddress (`str`)
421
+ * submissionId (`str`)
422
+ * submissionHash (`str`)
423
+ * tournamentId (`str`)
424
+ """
425
+ if (amount is None) == (max_amount is None):
426
+ raise ValueError("Provide exactly one of amount or max_amount.")
427
+
428
+ max_amount = max_amount if max_amount is not None else amount
429
+ query = """
430
+ mutation($submissionId: ID!, $staker: String!, $maxAmount: String!) {
431
+ v3StakeAuth(
432
+ submissionId: $submissionId
433
+ staker: $staker
434
+ maxAmount: $maxAmount
435
+ ) {
436
+ authorizationSigner
437
+ authorizationDigest
438
+ chainId
439
+ deadline
440
+ maxAmount
441
+ modelId
442
+ nmrAddress
443
+ nonce
444
+ roundId
445
+ signature
446
+ staker
447
+ stakingAddress
448
+ submissionId
449
+ submissionHash
450
+ tournamentId
451
+ }
452
+ }
453
+ """
454
+ arguments = {
455
+ "submissionId": submission_id,
456
+ "staker": staker,
457
+ "maxAmount": str(max_amount),
458
+ }
459
+ authorization = self.raw_query(query, arguments, authorization=True)["data"][
460
+ "v3StakeAuth"
461
+ ]
462
+ authorization["amount"] = authorization["maxAmount"]
463
+ return authorization
464
+
465
+ def v3_stake_config(self) -> Dict:
466
+ """Fetch staking v3 contract configuration.
467
+
468
+ Returns:
469
+ dict: staking v3 configuration with the following fields:
470
+
471
+ * address (`str`)
472
+ * authorizationSigner (`str`)
473
+ * nmrAddress (`str`)
474
+ * owner (`str`)
475
+ * paused (`bool`)
476
+ * pendingOwner (`str`)
477
+ * serviceWallet (`str`)
478
+ """
479
+ query = """
480
+ query {
481
+ v3StakeConfig {
482
+ address
483
+ authorizationSigner
484
+ nmrAddress
485
+ owner
486
+ paused
487
+ pendingOwner
488
+ serviceWallet
489
+ }
490
+ }
491
+ """
492
+ return self.raw_query(query, authorization=True)["data"]["v3StakeConfig"]
493
+
494
+ def v3_stake_round(self, round_id: int | str) -> Dict:
495
+ """Fetch staking v3 round status by round id.
496
+
497
+ Args:
498
+ round_id (int or str): round id
499
+
500
+ Returns:
501
+ dict: staking v3 round data with the following fields:
502
+
503
+ * closeTime (`str`)
504
+ * merkleRoot (`str`)
505
+ * openTime (`str`)
506
+ * payoutFactor (`str`)
507
+ * remainingBurn (`str`)
508
+ * remainingPayout (`str`)
509
+ * resolveTime (`str`)
510
+ * resolved (`bool`)
511
+ * roundId (`str`)
512
+ * stakeCap (`str`)
513
+ * stakeThreshold (`str`)
514
+ * state (`str`)
515
+ * totalPayout (`str`)
516
+ * totalStaked (`str`)
517
+ * tournamentId (`str`)
518
+ """
519
+ query = """
520
+ query($roundId: String!) {
521
+ v3StakeRound(roundId: $roundId) {
522
+ closeTime
523
+ merkleRoot
524
+ openTime
525
+ payoutFactor
526
+ remainingBurn
527
+ remainingPayout
528
+ resolveTime
529
+ resolved
530
+ roundId
531
+ stakeCap
532
+ stakeThreshold
533
+ state
534
+ totalPayout
535
+ totalStaked
536
+ tournamentId
537
+ }
538
+ }
539
+ """
540
+ arguments = {"roundId": str(round_id)}
541
+ return self.raw_query(query, arguments, authorization=True)["data"][
542
+ "v3StakeRound"
543
+ ]
544
+
545
+ def v3_stake_claim(self, round_id: int | str, model_id: str, staker: str) -> Dict:
546
+ """Fetch a staking v3 claim proof for a model and staker.
547
+
548
+ Args:
549
+ round_id (int or str): round id
550
+ model_id (str): model id
551
+ staker (str): staker wallet address
552
+
553
+ Returns:
554
+ dict: claim payload with the following fields:
555
+
556
+ * apiModelId (`str`)
557
+ * burnAmountWei (`str`)
558
+ * merkleRoot (`str`)
559
+ * modelId (`str`)
560
+ * payoutAmountWei (`str`)
561
+ * proof (`list` of `str`)
562
+ * roundId (`str`)
563
+ * staker (`str`)
564
+ * submissionId (`str`)
565
+ * tournamentId (`str`)
566
+ """
567
+ query = """
568
+ query($roundId: String!, $modelId: ID!, $staker: String!) {
569
+ v3StakeClaim(roundId: $roundId, modelId: $modelId, staker: $staker) {
570
+ apiModelId
571
+ burnAmountWei
572
+ merkleRoot
573
+ modelId
574
+ payoutAmountWei
575
+ proof
576
+ roundId
577
+ staker
578
+ submissionId
579
+ tournamentId
580
+ }
581
+ }
582
+ """
583
+ arguments = {
584
+ "roundId": str(round_id),
585
+ "modelId": model_id,
586
+ "staker": staker,
587
+ }
588
+ return self.raw_query(query, arguments, authorization=True)["data"][
589
+ "v3StakeClaim"
590
+ ]
591
+
386
592
  def get_current_round(self, tournament: int | None = None) -> int | None:
387
593
  """Get number of the current active round.
388
594
 
@@ -841,9 +1047,145 @@ class Api:
841
1047
  utils.replace(results, "updatedAt", utils.parse_datetime_string)
842
1048
  return results
843
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
+
844
1184
  def round_model_performances_v2(self, model_id: str):
845
1185
  """Fetch round model performance of a user.
846
1186
 
1187
+ DEPRECATED - please use `submission_scores` instead when possible.
1188
+
847
1189
  Args:
848
1190
  model_id (str)
849
1191
 
@@ -871,6 +1213,13 @@ class Api:
871
1213
  * percentile (`float`)
872
1214
  * value (`float`): value of the metric
873
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
+ )
874
1223
 
875
1224
  query = """
876
1225
  query($modelId: String!
@@ -1200,7 +1549,7 @@ class Api:
1200
1549
  return False
1201
1550
  if raw is None:
1202
1551
  return False
1203
- open_time = utils.parse_datetime_string(raw['openTime'])
1552
+ open_time = utils.parse_datetime_string(raw["openTime"])
1204
1553
  now = datetime.datetime.now(tz=pytz.utc)
1205
1554
  is_new_round = open_time > now - datetime.timedelta(hours=hours)
1206
1555
  return is_new_round
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numerapi
3
- Version: 2.22.0
3
+ Version: 2.23.0.dev1
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.22.0"
8
+ numerapi_version = "2.23.0.dev1"
9
9
 
10
10
 
11
11
  classifiers = [
@@ -0,0 +1,417 @@
1
+ import datetime
2
+ import decimal
3
+ import json
4
+ import os
5
+ import pytest
6
+ import responses
7
+
8
+ from numerapi import base_api
9
+
10
+
11
+ @pytest.fixture(scope='function', name="api")
12
+ def api_fixture():
13
+ api = base_api.Api(verbosity='DEBUG')
14
+ return api
15
+
16
+
17
+ def test_NumerAPI():
18
+ # invalid log level should raise
19
+ with pytest.raises(AttributeError):
20
+ base_api.Api(verbosity="FOO")
21
+
22
+
23
+ def test__login(api):
24
+ # passing only one of public_id and secret_key is not enough
25
+ api._login(public_id="foo", secret_key=None)
26
+ assert api.token is None
27
+ api._login(public_id=None, secret_key="bar")
28
+ assert api.token is None
29
+ # passing both works
30
+ api._login(public_id="foo", secret_key="bar")
31
+ assert api.token == ("foo", "bar")
32
+
33
+ # using env variables
34
+ os.environ["NUMERAI_SECRET_KEY"] = "key"
35
+ os.environ["NUMERAI_PUBLIC_ID"] = "id"
36
+ api._login()
37
+ assert api.token == ("id", "key")
38
+
39
+
40
+ @responses.activate
41
+ def test_raw_query(api):
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
+ )
48
+ result = api.raw_query(query)
49
+ assert isinstance(result, dict)
50
+ assert "data" in result
51
+
52
+
53
+ @responses.activate
54
+ def test_get_account(api):
55
+ api.token = ("", "")
56
+ account = {'apiTokens': [{'name': 'uploads',
57
+ 'public_id': 'AAA',
58
+ 'scopes': ['upload_submission']},
59
+ {'name': '_internal_default',
60
+ 'public_id': 'BBB',
61
+ 'scopes': ['upload_submission',
62
+ 'read_submission_info',
63
+ 'read_user_info']},
64
+ {'name': 'all',
65
+ 'public_id': 'CCC',
66
+ 'scopes': ['upload_submission',
67
+ 'stake',
68
+ 'read_submission_info',
69
+ 'read_user_info']}],
70
+ 'availableNmr': '1.010000000000000000',
71
+ 'email': 'no-reply@eu83t4nncmxv3g2.xyz',
72
+ 'id': '0c10a70a-a851-478f-a289-7a05fe397008',
73
+ 'insertedAt': "2018-01-01 11:11:11",
74
+ 'mfaEnabled': False,
75
+ 'models': [{'id': '881778ad-2ee9-4fb0-82b4-7b7c0f7ce17d',
76
+ 'name': 'model1',
77
+ 'submissions':
78
+ [{'filename': 'predictions-pPbLKSHGiR.csv',
79
+ 'id': 'f2369b69-8c43-47aa-b4de-de3a9de5f52c'}],
80
+ 'v2Stake': {'status': None, 'txHash': None}},
81
+ {'id': '881778ad-2ee9-4fb0-82b4-7b7c0f7ce17d',
82
+ 'name': 'model2',
83
+ 'submissions':
84
+ [{'filename': 'predictions-pPbPOWQNGiR.csv',
85
+ 'id': '46a62500-87c7-4d7c-98ad-b743037e8cfd'}],
86
+ 'v2Stake': {'status': None, 'txHash': None}}],
87
+ 'status': 'VERIFIED',
88
+ 'username': 'username1',
89
+ 'walletAddress': '0x0000000000000000000000000000'}
90
+
91
+ data = {'data': {'account': account}}
92
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
93
+ res = api.get_account()
94
+ assert isinstance(res, dict)
95
+ assert len(res.get('models')) == 2
96
+
97
+
98
+ @responses.activate
99
+ def test_get_models(api):
100
+ api.token = ("", "")
101
+ models_list = [
102
+ {"name": "model_x", "id": "95b0d9e2-c901-4f2b-9c98-24138b0bd706",
103
+ "tournament": 0},
104
+ {"name": "model_y", "id": "2c6d63a4-013f-42d1-bbaf-bf35725d29f7",
105
+ "tournament": 0}]
106
+ data = {'data': {'account': {'models': models_list}}}
107
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
108
+ models = api.get_models()
109
+ assert sorted(models.keys()) == ['model_x', 'model_y']
110
+ assert sorted(models.values()) == ['2c6d63a4-013f-42d1-bbaf-bf35725d29f7',
111
+ '95b0d9e2-c901-4f2b-9c98-24138b0bd706']
112
+
113
+
114
+ @responses.activate
115
+ def test_set_submission_webhook(api):
116
+ api.token = ("", "")
117
+ data = {
118
+ "data": {
119
+ "setSubmissionWebhook": "true"
120
+ }
121
+ }
122
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
123
+ res = api.set_submission_webhook(
124
+ '2c6d63a4-013f-42d1-bbaf-bf35725d29f7',
125
+ 'https://triggerurl'
126
+ )
127
+ assert res
128
+
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
+
275
+ @responses.activate
276
+ def test_v3_stake_auth(api):
277
+ api.token = ("", "")
278
+ data = {"data": {"v3StakeAuth": {
279
+ "authorizationSigner": "0xsigner",
280
+ "authorizationDigest": "0xdigest",
281
+ "chainId": "11155111",
282
+ "deadline": "1770000000",
283
+ "maxAmount": "25",
284
+ "modelId": "0xmodel",
285
+ "nmrAddress": "0xnmr",
286
+ "nonce": "0",
287
+ "roundId": "4321",
288
+ "signature": "0x1234",
289
+ "staker": "0xstaker",
290
+ "stakingAddress": "0xstaking",
291
+ "submissionId": "submission-id",
292
+ "submissionHash": "0xhash",
293
+ "tournamentId": "8",
294
+ }}}
295
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
296
+
297
+ result = api.v3_stake_auth("submission-id", "0xstaker", amount=25)
298
+
299
+ body = json.loads(responses.calls[0].request.body)
300
+ assert "v3StakeAuth" in body["query"]
301
+ assert body["variables"]["submissionId"] == "submission-id"
302
+ assert body["variables"]["maxAmount"] == "25"
303
+ assert "maxAmount" in body["query"]
304
+ assert result["maxAmount"] == "25"
305
+ assert result["amount"] == "25"
306
+ assert result["authorizationDigest"] == "0xdigest"
307
+
308
+
309
+ @responses.activate
310
+ def test_v3_stake_auth_accepts_max_amount(api):
311
+ api.token = ("", "")
312
+ data = {"data": {"v3StakeAuth": {
313
+ "authorizationSigner": "0xsigner",
314
+ "authorizationDigest": "0xdigest",
315
+ "chainId": "11155111",
316
+ "deadline": "1770000000",
317
+ "maxAmount": "30",
318
+ "modelId": "0xmodel",
319
+ "nmrAddress": "0xnmr",
320
+ "nonce": "0",
321
+ "roundId": "4321",
322
+ "signature": "0x1234",
323
+ "staker": "0xstaker",
324
+ "stakingAddress": "0xstaking",
325
+ "submissionId": "submission-id",
326
+ "submissionHash": "0xhash",
327
+ "tournamentId": "8",
328
+ }}}
329
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
330
+
331
+ result = api.v3_stake_auth(
332
+ "submission-id",
333
+ "0xstaker",
334
+ max_amount="30",
335
+ )
336
+
337
+ body = json.loads(responses.calls[0].request.body)
338
+ assert body["variables"]["maxAmount"] == "30"
339
+ assert result["maxAmount"] == "30"
340
+ assert result["amount"] == "30"
341
+
342
+
343
+ @responses.activate
344
+ def test_v3_stake_config(api):
345
+ api.token = ("", "")
346
+ data = {"data": {"v3StakeConfig": {
347
+ "address": "0xstaking",
348
+ "authorizationSigner": "0xsigner",
349
+ "nmrAddress": "0xnmr",
350
+ "owner": "0xowner",
351
+ "paused": False,
352
+ "pendingOwner": "0xpending",
353
+ "serviceWallet": "0xservice",
354
+ }}}
355
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
356
+
357
+ result = api.v3_stake_config()
358
+
359
+ body = json.loads(responses.calls[0].request.body)
360
+ assert "v3StakeConfig" in body["query"]
361
+ assert result["authorizationSigner"] == "0xsigner"
362
+
363
+
364
+ @responses.activate
365
+ def test_v3_stake_round(api):
366
+ api.token = ("", "")
367
+ data = {"data": {"v3StakeRound": {
368
+ "closeTime": "1",
369
+ "merkleRoot": "0xroot",
370
+ "openTime": "0",
371
+ "payoutFactor": "0.5",
372
+ "remainingBurn": "0",
373
+ "remainingPayout": "2",
374
+ "resolveTime": "2",
375
+ "resolved": False,
376
+ "roundId": "4321",
377
+ "stakeCap": "100",
378
+ "stakeThreshold": "10",
379
+ "state": "open",
380
+ "totalPayout": "2",
381
+ "totalStaked": "50",
382
+ "tournamentId": "8",
383
+ }}}
384
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
385
+
386
+ result = api.v3_stake_round(4321)
387
+
388
+ body = json.loads(responses.calls[0].request.body)
389
+ assert "v3StakeRound" in body["query"]
390
+ assert body["variables"]["roundId"] == "4321"
391
+ assert result["roundId"] == "4321"
392
+
393
+
394
+ @responses.activate
395
+ def test_v3_stake_claim(api):
396
+ api.token = ("", "")
397
+ data = {"data": {"v3StakeClaim": {
398
+ "apiModelId": "api-model-id",
399
+ "burnAmountWei": "0",
400
+ "merkleRoot": "0xroot",
401
+ "modelId": "0xmodel",
402
+ "payoutAmountWei": "2000000000000000000",
403
+ "proof": ["0xproof"],
404
+ "roundId": "4321",
405
+ "staker": "0xstaker",
406
+ "submissionId": "submission-id",
407
+ "tournamentId": "8",
408
+ }}}
409
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
410
+
411
+ result = api.v3_stake_claim(4321, "api-model-id", "0xstaker")
412
+
413
+ body = json.loads(responses.calls[0].request.body)
414
+ assert "v3StakeClaim" in body["query"]
415
+ assert body["variables"]["roundId"] == "4321"
416
+ assert body["variables"]["modelId"] == "api-model-id"
417
+ assert result["proof"] == ["0xproof"]
@@ -1,5 +1,6 @@
1
1
  import pytest
2
2
  import datetime
3
+ import json
3
4
  import pytz
4
5
  import responses
5
6
 
@@ -15,17 +16,20 @@ def api_fixture():
15
16
  return api
16
17
 
17
18
 
19
+ @pytest.mark.live_api
18
20
  def test_get_competitions(api):
19
21
  res = api.get_competitions(tournament=1)
20
22
  assert isinstance(res, list)
21
23
  assert len(res) > 80
22
24
 
23
25
 
26
+ @pytest.mark.live_api
24
27
  def test_get_current_round(api):
25
28
  current_round = api.get_current_round()
26
29
  assert current_round >= 82
27
30
 
28
31
 
32
+ @pytest.mark.live_api
29
33
  @pytest.mark.parametrize("fun", ["get_account", "wallet_transactions"])
30
34
  def test_unauthorized_requests(api, fun):
31
35
  with pytest.raises(ValueError) as err:
@@ -37,6 +41,7 @@ def test_unauthorized_requests(api, fun):
37
41
  "Your session is invalid or has expired." in str(err.value)
38
42
 
39
43
 
44
+ @pytest.mark.live_api
40
45
  def test_error_handling(api):
41
46
  # String instead of Int
42
47
  with pytest.raises(ValueError):
@@ -99,3 +104,4 @@ def test_check_new_round(api):
99
104
  assert api.check_new_round()
100
105
  # second
101
106
  assert not api.check_new_round()
107
+
@@ -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
@@ -1,118 +0,0 @@
1
- import os
2
- import pytest
3
- import responses
4
-
5
- from numerapi import base_api
6
-
7
-
8
- @pytest.fixture(scope='function', name="api")
9
- def api_fixture():
10
- api = base_api.Api(verbosity='DEBUG')
11
- return api
12
-
13
-
14
- def test_NumerAPI():
15
- # invalid log level should raise
16
- with pytest.raises(AttributeError):
17
- base_api.Api(verbosity="FOO")
18
-
19
-
20
- def test__login(api):
21
- # passing only one of public_id and secret_key is not enough
22
- api._login(public_id="foo", secret_key=None)
23
- assert api.token is None
24
- api._login(public_id=None, secret_key="bar")
25
- assert api.token is None
26
- # passing both works
27
- api._login(public_id="foo", secret_key="bar")
28
- assert api.token == ("foo", "bar")
29
-
30
- # using env variables
31
- os.environ["NUMERAI_SECRET_KEY"] = "key"
32
- os.environ["NUMERAI_PUBLIC_ID"] = "id"
33
- api._login()
34
- assert api.token == ("id", "key")
35
-
36
-
37
- def test_raw_query(api):
38
- query = "query {latestNmrPrice {priceUsd}}"
39
- result = api.raw_query(query)
40
- assert isinstance(result, dict)
41
- assert "data" in result
42
-
43
-
44
- @responses.activate
45
- def test_get_account(api):
46
- api.token = ("", "")
47
- account = {'apiTokens': [{'name': 'uploads',
48
- 'public_id': 'AAA',
49
- 'scopes': ['upload_submission']},
50
- {'name': '_internal_default',
51
- 'public_id': 'BBB',
52
- 'scopes': ['upload_submission',
53
- 'read_submission_info',
54
- 'read_user_info']},
55
- {'name': 'all',
56
- 'public_id': 'CCC',
57
- 'scopes': ['upload_submission',
58
- 'stake',
59
- 'read_submission_info',
60
- 'read_user_info']}],
61
- 'availableNmr': '1.010000000000000000',
62
- 'email': 'no-reply@eu83t4nncmxv3g2.xyz',
63
- 'id': '0c10a70a-a851-478f-a289-7a05fe397008',
64
- 'insertedAt': "2018-01-01 11:11:11",
65
- 'mfaEnabled': False,
66
- 'models': [{'id': '881778ad-2ee9-4fb0-82b4-7b7c0f7ce17d',
67
- 'name': 'model1',
68
- 'submissions':
69
- [{'filename': 'predictions-pPbLKSHGiR.csv',
70
- 'id': 'f2369b69-8c43-47aa-b4de-de3a9de5f52c'}],
71
- 'v2Stake': {'status': None, 'txHash': None}},
72
- {'id': '881778ad-2ee9-4fb0-82b4-7b7c0f7ce17d',
73
- 'name': 'model2',
74
- 'submissions':
75
- [{'filename': 'predictions-pPbPOWQNGiR.csv',
76
- 'id': '46a62500-87c7-4d7c-98ad-b743037e8cfd'}],
77
- 'v2Stake': {'status': None, 'txHash': None}}],
78
- 'status': 'VERIFIED',
79
- 'username': 'username1',
80
- 'walletAddress': '0x0000000000000000000000000000'}
81
-
82
- data = {'data': {'account': account}}
83
- responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
84
- res = api.get_account()
85
- assert isinstance(res, dict)
86
- assert len(res.get('models')) == 2
87
-
88
-
89
- @responses.activate
90
- def test_get_models(api):
91
- api.token = ("", "")
92
- models_list = [
93
- {"name": "model_x", "id": "95b0d9e2-c901-4f2b-9c98-24138b0bd706",
94
- "tournament": 0},
95
- {"name": "model_y", "id": "2c6d63a4-013f-42d1-bbaf-bf35725d29f7",
96
- "tournament": 0}]
97
- data = {'data': {'account': {'models': models_list}}}
98
- responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
99
- models = api.get_models()
100
- assert sorted(models.keys()) == ['model_x', 'model_y']
101
- assert sorted(models.values()) == ['2c6d63a4-013f-42d1-bbaf-bf35725d29f7',
102
- '95b0d9e2-c901-4f2b-9c98-24138b0bd706']
103
-
104
-
105
- @responses.activate
106
- def test_set_submission_webhook(api):
107
- api.token = ("", "")
108
- data = {
109
- "data": {
110
- "setSubmissionWebhook": "true"
111
- }
112
- }
113
- responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
114
- res = api.set_submission_webhook(
115
- '2c6d63a4-013f-42d1-bbaf-bf35725d29f7',
116
- 'https://triggerurl'
117
- )
118
- assert res
File without changes
File without changes
File without changes