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.
- {numerapi-2.22.0 → numerapi-2.23.0}/PKG-INFO +1 -1
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/base_api.py +422 -1
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/PKG-INFO +1 -1
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/SOURCES.txt +1 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/setup.py +1 -1
- numerapi-2.23.0/tests/test_base_api.py +484 -0
- numerapi-2.23.0/tests/test_cryptoapi.py +43 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/tests/test_numerapi.py +6 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/tests/test_signalsapi.py +1 -0
- numerapi-2.22.0/tests/test_base_api.py +0 -118
- {numerapi-2.22.0 → numerapi-2.23.0}/LICENSE +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/README.md +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/__init__.py +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/cli.py +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/cryptoapi.py +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/numerapi.py +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/py.typed +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/signalsapi.py +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi/utils.py +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/dependency_links.txt +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/entry_points.txt +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/requires.txt +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/numerapi.egg-info/top_level.txt +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/setup.cfg +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/tests/test_cli.py +0 -0
- {numerapi-2.22.0 → numerapi-2.23.0}/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
|
|
|
@@ -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[
|
|
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
|
|
@@ -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
|
+
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|