numerapi 2.22.0__tar.gz → 2.23.0__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 (26) hide show
  1. {numerapi-2.22.0 → numerapi-2.23.0}/PKG-INFO +1 -1
  2. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/base_api.py +422 -1
  3. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/PKG-INFO +1 -1
  4. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/SOURCES.txt +1 -0
  5. {numerapi-2.22.0 → numerapi-2.23.0}/setup.py +1 -1
  6. numerapi-2.23.0/tests/test_base_api.py +484 -0
  7. numerapi-2.23.0/tests/test_cryptoapi.py +43 -0
  8. {numerapi-2.22.0 → numerapi-2.23.0}/tests/test_numerapi.py +6 -0
  9. {numerapi-2.22.0 → numerapi-2.23.0}/tests/test_signalsapi.py +1 -0
  10. numerapi-2.22.0/tests/test_base_api.py +0 -118
  11. {numerapi-2.22.0 → numerapi-2.23.0}/LICENSE +0 -0
  12. {numerapi-2.22.0 → numerapi-2.23.0}/README.md +0 -0
  13. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/__init__.py +0 -0
  14. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/cli.py +0 -0
  15. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/cryptoapi.py +0 -0
  16. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/numerapi.py +0 -0
  17. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/py.typed +0 -0
  18. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/signalsapi.py +0 -0
  19. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/utils.py +0 -0
  20. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/dependency_links.txt +0 -0
  21. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/entry_points.txt +0 -0
  22. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/requires.txt +0 -0
  23. {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/top_level.txt +0 -0
  24. {numerapi-2.22.0 → numerapi-2.23.0}/setup.cfg +0 -0
  25. {numerapi-2.22.0 → numerapi-2.23.0}/tests/test_cli.py +0 -0
  26. {numerapi-2.22.0 → numerapi-2.23.0}/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
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
+ query($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
 
@@ -414,6 +620,79 @@ class Api:
414
620
  round_num = data["number"]
415
621
  return round_num
416
622
 
623
+ def list_rounds(
624
+ self,
625
+ number: int | None = None,
626
+ target: str | None = None,
627
+ status: str | None = None,
628
+ limit: int | None = None,
629
+ ) -> List[Dict]:
630
+ """List rounds with the filters supported by the round resolver.
631
+
632
+ Args:
633
+ number (int, optional): round number filter
634
+ target (str, optional): round target filter
635
+ status (str, optional): round status filter. One of `upcoming`,
636
+ `open`, `resolving`, or `resolved`
637
+ limit (int, optional): maximum number of rounds to return
638
+
639
+ Returns:
640
+ list of dicts: round entries matching the provided filters
641
+ """
642
+ query = """
643
+ query($tournament: Int
644
+ $number: Int
645
+ $target: String
646
+ $status: RoundStatus
647
+ $limit: Int) {
648
+ rounds(tournament: $tournament
649
+ number: $number
650
+ target: $target
651
+ status: $status
652
+ limit: $limit) {
653
+ id
654
+ tournament
655
+ number
656
+ target
657
+ closeTime
658
+ closeStakingTime
659
+ openTime
660
+ scoreTime
661
+ resolveTime
662
+ resolvedGeneral
663
+ resolvedStaking
664
+ payoutFactor
665
+ stakeThreshold
666
+ minCorrMultiplier
667
+ maxCorrMultiplier
668
+ defaultCorrMultiplier
669
+ minMmcMultiplier
670
+ maxMmcMultiplier
671
+ defaultMmcMultiplier
672
+ dataDatestamp
673
+ }
674
+ }
675
+ """
676
+ arguments = {
677
+ "tournament": self.tournament_id,
678
+ "number": number,
679
+ "target": target,
680
+ "status": None if status is None else status.upper(),
681
+ "limit": limit,
682
+ }
683
+ rounds = self.raw_query(query, arguments)["data"]["rounds"]
684
+ for round_info in rounds:
685
+ for field in [
686
+ "closeTime",
687
+ "closeStakingTime",
688
+ "openTime",
689
+ "scoreTime",
690
+ "resolveTime",
691
+ ]:
692
+ utils.replace(round_info, field, utils.parse_datetime_string)
693
+ utils.replace(round_info, "payoutFactor", utils.parse_float_string)
694
+ return rounds
695
+
417
696
  def set_bio(self, model_id: str, bio: str) -> bool:
418
697
  """Set bio field for a model id.
419
698
 
@@ -841,9 +1120,144 @@ class Api:
841
1120
  utils.replace(results, "updatedAt", utils.parse_datetime_string)
842
1121
  return results
843
1122
 
1123
+ def submission_scores(
1124
+ self,
1125
+ model_id: str,
1126
+ display_name: str | None = None,
1127
+ version: str | None = None,
1128
+ day: int | None = None,
1129
+ resolved: bool | None = None,
1130
+ last_n_rounds: int | None = None,
1131
+ distinct_on_round: bool | None = None,
1132
+ ) -> List[Dict]:
1133
+ """Fetch submission score history for a model.
1134
+
1135
+ Args:
1136
+ model_id (str): target model UUID
1137
+ display_name (str, optional): score metric name filter
1138
+ version (str, optional): score version filter
1139
+ day (int, optional): day filter
1140
+ resolved (bool, optional): resolved-state filter
1141
+ tournament (int, optional): tournament filter, defaults to the
1142
+ API instance tournament
1143
+ last_n_rounds (int, optional): limit by most recent rounds
1144
+ distinct_on_round (bool, optional): keep only the latest score per
1145
+ round after applying other filters
1146
+
1147
+ Returns:
1148
+ list of dicts: list of submission score entries
1149
+ """
1150
+
1151
+ query = """
1152
+ query($modelId: ID!
1153
+ $displayName: String
1154
+ $version: String
1155
+ $day: Int
1156
+ $resolved: Boolean
1157
+ $tournament: Int
1158
+ $lastNRounds: Int
1159
+ $distinctOnRound: Boolean) {
1160
+ submissionScores(modelId: $modelId
1161
+ displayName: $displayName
1162
+ version: $version
1163
+ day: $day
1164
+ resolved: $resolved
1165
+ tournament: $tournament
1166
+ lastNRounds: $lastNRounds
1167
+ distinctOnRound: $distinctOnRound) {
1168
+ roundId
1169
+ submissionId
1170
+ roundNumber
1171
+ roundResolveTime
1172
+ roundScoreTime
1173
+ roundCloseStakingTime
1174
+ value
1175
+ percentile
1176
+ displayName
1177
+ version
1178
+ date
1179
+ day
1180
+ resolveDate
1181
+ resolved
1182
+ }
1183
+ }
1184
+ """
1185
+ arguments = {
1186
+ "modelId": model_id,
1187
+ "displayName": display_name,
1188
+ "version": version,
1189
+ "day": day,
1190
+ "resolved": resolved,
1191
+ "tournament": self.tournament_id,
1192
+ "lastNRounds": last_n_rounds,
1193
+ "distinctOnRound": distinct_on_round,
1194
+ }
1195
+ scores = self.raw_query(query, arguments)["data"]["submissionScores"]
1196
+ for score in scores:
1197
+ utils.replace(score, "roundResolveTime", utils.parse_datetime_string)
1198
+ utils.replace(score, "roundScoreTime", utils.parse_datetime_string)
1199
+ utils.replace(score, "roundCloseStakingTime", utils.parse_datetime_string)
1200
+ utils.replace(score, "date", utils.parse_datetime_string)
1201
+ utils.replace(score, "resolveDate", utils.parse_datetime_string)
1202
+ return scores
1203
+
1204
+ def pending_model_payouts(self, tournament: int | None = None) -> Dict:
1205
+ """Fetch actual and pending payouts for the authenticated user's models.
1206
+
1207
+ Args:
1208
+ tournament (int, optional): tournament filter, defaults to the API
1209
+ instance tournament
1210
+
1211
+ Returns:
1212
+ dict: payout groups with `actual` and `pending` lists
1213
+ """
1214
+
1215
+ query = """
1216
+ query($tournament: Int!) {
1217
+ pendingModelPayouts(tournament: $tournament) {
1218
+ actual {
1219
+ roundId
1220
+ roundNumber
1221
+ roundResolveTime
1222
+ modelId
1223
+ modelName
1224
+ modelDisplayName
1225
+ payoutNmr
1226
+ payoutValue
1227
+ currencySymbol
1228
+ }
1229
+ pending {
1230
+ roundId
1231
+ roundNumber
1232
+ roundResolveTime
1233
+ modelId
1234
+ modelName
1235
+ modelDisplayName
1236
+ payoutNmr
1237
+ payoutValue
1238
+ currencySymbol
1239
+ }
1240
+ }
1241
+ }
1242
+ """
1243
+ arguments = {
1244
+ "tournament": self.tournament_id if tournament is None else tournament
1245
+ }
1246
+ payouts = self.raw_query(query, arguments, authorization=True)["data"][
1247
+ "pendingModelPayouts"
1248
+ ]
1249
+ for payout_type in ["actual", "pending"]:
1250
+ for payout in payouts[payout_type]:
1251
+ utils.replace(payout, "roundResolveTime", utils.parse_datetime_string)
1252
+ utils.replace(payout, "payoutNmr", utils.parse_float_string)
1253
+ utils.replace(payout, "payoutValue", utils.parse_float_string)
1254
+ return payouts
1255
+
844
1256
  def round_model_performances_v2(self, model_id: str):
845
1257
  """Fetch round model performance of a user.
846
1258
 
1259
+ DEPRECATED - please use `submission_scores` instead when possible.
1260
+
847
1261
  Args:
848
1262
  model_id (str)
849
1263
 
@@ -871,6 +1285,13 @@ class Api:
871
1285
  * percentile (`float`)
872
1286
  * value (`float`): value of the metric
873
1287
  """
1288
+ warnings.warn(
1289
+ "`round_model_performances_v2` is deprecated because it relies on "
1290
+ "`v2RoundModelPerformances`. Use `submission_scores` instead when "
1291
+ "possible.",
1292
+ DeprecationWarning,
1293
+ stacklevel=2,
1294
+ )
874
1295
 
875
1296
  query = """
876
1297
  query($modelId: String!
@@ -1200,7 +1621,7 @@ class Api:
1200
1621
  return False
1201
1622
  if raw is None:
1202
1623
  return False
1203
- open_time = utils.parse_datetime_string(raw['openTime'])
1624
+ open_time = utils.parse_datetime_string(raw["openTime"])
1204
1625
  now = datetime.datetime.now(tz=pytz.utc)
1205
1626
  is_new_round = open_time > now - datetime.timedelta(hours=hours)
1206
1627
  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
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
@@ -17,6 +17,7 @@ numerapi.egg-info/requires.txt
17
17
  numerapi.egg-info/top_level.txt
18
18
  tests/test_base_api.py
19
19
  tests/test_cli.py
20
+ tests/test_cryptoapi.py
20
21
  tests/test_numerapi.py
21
22
  tests/test_signalsapi.py
22
23
  tests/test_utils.py
@@ -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"
9
9
 
10
10
 
11
11
  classifiers = [
@@ -0,0 +1,484 @@
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_list_rounds(api):
183
+ api.tournament_id = 11
184
+ data = {
185
+ "data": {
186
+ "rounds": [
187
+ {
188
+ "id": "round-1",
189
+ "tournament": 11,
190
+ "number": 123,
191
+ "target": "main",
192
+ "closeTime": "2026-03-27T00:00:00Z",
193
+ "closeStakingTime": "2026-03-26T12:00:00Z",
194
+ "openTime": "2026-03-20T00:00:00Z",
195
+ "scoreTime": "2026-03-29T00:00:00Z",
196
+ "resolveTime": "2026-04-01T00:00:00Z",
197
+ "resolvedGeneral": False,
198
+ "resolvedStaking": False,
199
+ "payoutFactor": "0.8",
200
+ "stakeThreshold": 0.1,
201
+ "minCorrMultiplier": 0.0,
202
+ "maxCorrMultiplier": 1.0,
203
+ "defaultCorrMultiplier": 0.5,
204
+ "minMmcMultiplier": 0.0,
205
+ "maxMmcMultiplier": 1.0,
206
+ "defaultMmcMultiplier": 0.5,
207
+ "dataDatestamp": 20260320,
208
+ }
209
+ ]
210
+ }
211
+ }
212
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
213
+
214
+ res = api.list_rounds(number=123, target="main", status="open", limit=5)
215
+
216
+ assert len(res) == 1
217
+ assert isinstance(res[0]["closeTime"], datetime.datetime)
218
+ assert isinstance(res[0]["closeStakingTime"], datetime.datetime)
219
+ assert isinstance(res[0]["openTime"], datetime.datetime)
220
+ assert isinstance(res[0]["scoreTime"], datetime.datetime)
221
+ assert isinstance(res[0]["resolveTime"], datetime.datetime)
222
+ assert isinstance(res[0]["payoutFactor"], decimal.Decimal)
223
+
224
+ request_body = json.loads(responses.calls[0].request.body)
225
+ assert request_body["variables"]["tournament"] == 11
226
+ assert request_body["variables"]["number"] == 123
227
+ assert request_body["variables"]["target"] == "main"
228
+ assert request_body["variables"]["status"] == "OPEN"
229
+ assert request_body["variables"]["limit"] == 5
230
+
231
+
232
+ @responses.activate
233
+ def test_list_rounds_uses_api_tournament_id_by_default(api):
234
+ api.tournament_id = 11
235
+ data = {"data": {"rounds": []}}
236
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
237
+
238
+ api.list_rounds()
239
+
240
+ request_body = json.loads(responses.calls[0].request.body)
241
+ assert request_body["variables"]["tournament"] == 11
242
+
243
+
244
+ @responses.activate
245
+ def test_pending_model_payouts(api):
246
+ api.token = ("", "")
247
+ api.tournament_id = 12
248
+ data = {
249
+ "data": {
250
+ "pendingModelPayouts": {
251
+ "actual": [
252
+ {
253
+ "roundId": "round-a",
254
+ "roundNumber": 12,
255
+ "roundResolveTime": "2026-04-02T00:00:00Z",
256
+ "modelId": "model-a",
257
+ "modelName": "alpha",
258
+ "modelDisplayName": "Alpha",
259
+ "payoutNmr": "2.5000",
260
+ "payoutValue": "31.20",
261
+ "currencySymbol": "$",
262
+ }
263
+ ],
264
+ "pending": [
265
+ {
266
+ "roundId": "round-b",
267
+ "roundNumber": 13,
268
+ "roundResolveTime": "2026-04-09T00:00:00Z",
269
+ "modelId": "model-b",
270
+ "modelName": "beta",
271
+ "modelDisplayName": "Beta",
272
+ "payoutNmr": "1.1000",
273
+ "payoutValue": "13.73",
274
+ "currencySymbol": "$",
275
+ }
276
+ ],
277
+ }
278
+ }
279
+ }
280
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
281
+
282
+ res = api.pending_model_payouts()
283
+
284
+ assert len(res["actual"]) == 1
285
+ assert len(res["pending"]) == 1
286
+ assert res["actual"][0]["payoutNmr"] == decimal.Decimal("2.5000")
287
+ assert res["pending"][0]["payoutValue"] == decimal.Decimal("13.73")
288
+ assert isinstance(res["actual"][0]["roundResolveTime"], datetime.datetime)
289
+ assert isinstance(res["pending"][0]["roundResolveTime"], datetime.datetime)
290
+
291
+ request_body = json.loads(responses.calls[0].request.body)
292
+ assert request_body["variables"]["tournament"] == 12
293
+
294
+
295
+ @responses.activate
296
+ def test_round_model_performances_v2_warns(api):
297
+ api.tournament_id = 8
298
+ data = {
299
+ "data": {
300
+ "v2RoundModelPerformances": [
301
+ {
302
+ "atRisk": "10.5",
303
+ "corrMultiplier": 1.0,
304
+ "mmcMultiplier": 0.5,
305
+ "roundPayoutFactor": "0.8",
306
+ "roundNumber": 456,
307
+ "roundOpenTime": "2026-03-01T00:00:00Z",
308
+ "roundResolveTime": "2026-03-29T00:00:00Z",
309
+ "roundResolved": True,
310
+ "roundTarget": "main",
311
+ "submissionScores": [
312
+ {
313
+ "date": "2026-03-28T00:00:00Z",
314
+ "day": 20,
315
+ "displayName": "CORR20",
316
+ "payoutPending": "0.8",
317
+ "payoutSettled": "0.7",
318
+ "percentile": 0.9,
319
+ "value": 0.12,
320
+ }
321
+ ],
322
+ }
323
+ ]
324
+ }
325
+ }
326
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
327
+
328
+ with pytest.warns(DeprecationWarning, match="round_model_performances_v2"):
329
+ res = api.round_model_performances_v2("model-1")
330
+
331
+ assert len(res) == 1
332
+ assert res[0]["atRisk"] == decimal.Decimal("10.5")
333
+ assert isinstance(res[0]["roundOpenTime"], datetime.datetime)
334
+ assert isinstance(res[0]["roundResolveTime"], datetime.datetime)
335
+ assert res[0]["submissionScores"][0]["payoutPending"] == decimal.Decimal("0.8")
336
+
337
+
338
+ @responses.activate
339
+ def test_v3_stake_auth(api):
340
+ api.token = ("", "")
341
+ data = {"data": {"v3StakeAuth": {
342
+ "authorizationSigner": "0xsigner",
343
+ "authorizationDigest": "0xdigest",
344
+ "chainId": "11155111",
345
+ "deadline": "1770000000",
346
+ "maxAmount": "25",
347
+ "modelId": "0xmodel",
348
+ "nmrAddress": "0xnmr",
349
+ "nonce": "0",
350
+ "roundId": "4321",
351
+ "signature": "0x1234",
352
+ "staker": "0xstaker",
353
+ "stakingAddress": "0xstaking",
354
+ "submissionId": "submission-id",
355
+ "submissionHash": "0xhash",
356
+ "tournamentId": "8",
357
+ }}}
358
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
359
+
360
+ result = api.v3_stake_auth("submission-id", "0xstaker", amount=25)
361
+
362
+ body = json.loads(responses.calls[0].request.body)
363
+ assert "mutation" not in body["query"]
364
+ assert "query(" in body["query"]
365
+ assert "v3StakeAuth" in body["query"]
366
+ assert body["variables"]["submissionId"] == "submission-id"
367
+ assert body["variables"]["maxAmount"] == "25"
368
+ assert "maxAmount" in body["query"]
369
+ assert result["maxAmount"] == "25"
370
+ assert result["amount"] == "25"
371
+ assert result["authorizationDigest"] == "0xdigest"
372
+
373
+
374
+ @responses.activate
375
+ def test_v3_stake_auth_accepts_max_amount(api):
376
+ api.token = ("", "")
377
+ data = {"data": {"v3StakeAuth": {
378
+ "authorizationSigner": "0xsigner",
379
+ "authorizationDigest": "0xdigest",
380
+ "chainId": "11155111",
381
+ "deadline": "1770000000",
382
+ "maxAmount": "30",
383
+ "modelId": "0xmodel",
384
+ "nmrAddress": "0xnmr",
385
+ "nonce": "0",
386
+ "roundId": "4321",
387
+ "signature": "0x1234",
388
+ "staker": "0xstaker",
389
+ "stakingAddress": "0xstaking",
390
+ "submissionId": "submission-id",
391
+ "submissionHash": "0xhash",
392
+ "tournamentId": "8",
393
+ }}}
394
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
395
+
396
+ result = api.v3_stake_auth(
397
+ "submission-id",
398
+ "0xstaker",
399
+ max_amount="30",
400
+ )
401
+
402
+ body = json.loads(responses.calls[0].request.body)
403
+ assert "mutation" not in body["query"]
404
+ assert "query(" in body["query"]
405
+ assert body["variables"]["maxAmount"] == "30"
406
+ assert result["maxAmount"] == "30"
407
+ assert result["amount"] == "30"
408
+
409
+
410
+ @responses.activate
411
+ def test_v3_stake_config(api):
412
+ api.token = ("", "")
413
+ data = {"data": {"v3StakeConfig": {
414
+ "address": "0xstaking",
415
+ "authorizationSigner": "0xsigner",
416
+ "nmrAddress": "0xnmr",
417
+ "owner": "0xowner",
418
+ "paused": False,
419
+ "pendingOwner": "0xpending",
420
+ "serviceWallet": "0xservice",
421
+ }}}
422
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
423
+
424
+ result = api.v3_stake_config()
425
+
426
+ body = json.loads(responses.calls[0].request.body)
427
+ assert "v3StakeConfig" in body["query"]
428
+ assert result["authorizationSigner"] == "0xsigner"
429
+
430
+
431
+ @responses.activate
432
+ def test_v3_stake_round(api):
433
+ api.token = ("", "")
434
+ data = {"data": {"v3StakeRound": {
435
+ "closeTime": "1",
436
+ "merkleRoot": "0xroot",
437
+ "openTime": "0",
438
+ "payoutFactor": "0.5",
439
+ "remainingBurn": "0",
440
+ "remainingPayout": "2",
441
+ "resolveTime": "2",
442
+ "resolved": False,
443
+ "roundId": "4321",
444
+ "stakeCap": "100",
445
+ "stakeThreshold": "10",
446
+ "state": "open",
447
+ "totalPayout": "2",
448
+ "totalStaked": "50",
449
+ "tournamentId": "8",
450
+ }}}
451
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
452
+
453
+ result = api.v3_stake_round(4321)
454
+
455
+ body = json.loads(responses.calls[0].request.body)
456
+ assert "v3StakeRound" in body["query"]
457
+ assert body["variables"]["roundId"] == "4321"
458
+ assert result["roundId"] == "4321"
459
+
460
+
461
+ @responses.activate
462
+ def test_v3_stake_claim(api):
463
+ api.token = ("", "")
464
+ data = {"data": {"v3StakeClaim": {
465
+ "apiModelId": "api-model-id",
466
+ "burnAmountWei": "0",
467
+ "merkleRoot": "0xroot",
468
+ "modelId": "0xmodel",
469
+ "payoutAmountWei": "2000000000000000000",
470
+ "proof": ["0xproof"],
471
+ "roundId": "4321",
472
+ "staker": "0xstaker",
473
+ "submissionId": "submission-id",
474
+ "tournamentId": "8",
475
+ }}}
476
+ responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data)
477
+
478
+ result = api.v3_stake_claim(4321, "api-model-id", "0xstaker")
479
+
480
+ body = json.loads(responses.calls[0].request.body)
481
+ assert "v3StakeClaim" in body["query"]
482
+ assert body["variables"]["roundId"] == "4321"
483
+ assert body["variables"]["modelId"] == "api-model-id"
484
+ assert result["proof"] == ["0xproof"]
@@ -0,0 +1,43 @@
1
+ import decimal
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+
6
+ import numerapi
7
+
8
+
9
+ @pytest.fixture(scope="function", name="api")
10
+ def api_fixture():
11
+ api = numerapi.CryptoAPI(verbosity="DEBUG")
12
+ return api
13
+
14
+
15
+ @patch("numerapi.cryptoapi.CryptoAPI.raw_query")
16
+ def test_get_leaderboard(mocked, api):
17
+ mocked.return_value = {
18
+ "data": {
19
+ "cryptosignalsLeaderboard": [
20
+ {
21
+ "nmrStaked": "13.0",
22
+ "rank": 1,
23
+ "username": "crypto_user",
24
+ "corrRep": 0.1,
25
+ "mmcRep": 0.2,
26
+ "return_1_day": 0.03,
27
+ "return_52_weeks": 0.4,
28
+ "return_13_weeks": 0.15,
29
+ }
30
+ ]
31
+ }
32
+ }
33
+
34
+ lb = api.get_leaderboard(1)
35
+
36
+ assert len(lb) == 1
37
+ assert lb[0]["username"] == "crypto_user"
38
+ assert lb[0]["nmrStaked"] == decimal.Decimal("13.0")
39
+ mocked.assert_called_once()
40
+ args, kwargs = mocked.call_args
41
+ assert "cryptosignalsLeaderboard" in args[0]
42
+ assert args[1] == {"limit": 1, "offset": 0}
43
+ assert kwargs == {}
@@ -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
File without changes
File without changes
File without changes
File without changes
File without changes