├── .github └── workflows │ ├── pylint.yml │ ├── pypi.yml │ └── pytest.yml ├── .gitignore ├── .pylintrc ├── .readthedocs.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── Makefile ├── changelog.md ├── conf.py ├── index.rst ├── license.md └── requirements.txt ├── numerapi ├── __init__.py ├── base_api.py ├── cli.py ├── cryptoapi.py ├── numerapi.py ├── py.typed ├── signalsapi.py └── utils.py ├── requirements.txt ├── requirements_tests.txt ├── setup.py └── tests ├── test_base_api.py ├── test_cli.py ├── test_numerapi.py ├── test_signalsapi.py └── test_utils.py /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 3.10 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: "3.10" 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install pylint 19 | pip install -r requirements.txt 20 | - name: Analysing the code with pylint 21 | run: | 22 | python -m pylint `find numerapi/ -regextype egrep -regex '(.*.py)$'` 23 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build-n-publish: 10 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Set up Python 3.10 15 | uses: actions/setup-python@v2.2.2 16 | with: 17 | python-version: "3.10" 18 | - name: Install pypa/build 19 | run: >- 20 | python -m 21 | pip install 22 | build wheel 23 | --user 24 | - name: Build a binary wheel and a source tarball 25 | run: >- 26 | python setup.py sdist && python setup.py bdist_wheel 27 | - name: Publish distribution 📦 to PyPI 28 | if: startsWith(github.ref, 'refs/tags') 29 | uses: pypa/gh-action-pypi-publish@master 30 | with: 31 | password: ${{ secrets.PYPI_API_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Pytest 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 3.10 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: "3.10" 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install -r requirements.txt 19 | pip install -r requirements_tests.txt 20 | - name: Run tests 21 | run: python -m pytest --import-mode=append tests/ --cov=./ --cov-report=xml 22 | - name: Upload coverage to Codecov 23 | uses: codecov/codecov-action@v2 24 | with: 25 | directory: ./coverage/reports/ 26 | fail_ci_if_error: false 27 | verbose: true 28 | files: ./coverage.xml 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Jupyter Notebook 60 | .ipynb_checkpoints 61 | 62 | # pyenv 63 | .python-version 64 | 65 | # celery beat schedule file 66 | celerybeat-schedule 67 | 68 | # SageMath parsed files 69 | *.sage.py 70 | 71 | # dotenv 72 | .env 73 | 74 | # virtualenv 75 | .venv 76 | venv/ 77 | ENV/ 78 | 79 | # Spyder project settings 80 | .spyderproject 81 | 82 | # Rope project settings 83 | .ropeproject 84 | 85 | # mkdocs documentation 86 | /site 87 | 88 | # vim 89 | .session.vim 90 | .swp 91 | 92 | numerai_datasets/ 93 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-allow-list= 7 | 8 | # A comma-separated list of package or module names from where C extensions may 9 | # be loaded. Extensions are loading into the active Python interpreter and may 10 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list 11 | # for backward compatibility.) 12 | extension-pkg-whitelist= 13 | 14 | # Return non-zero exit code if any of these messages/categories are detected, 15 | # even if score is above --fail-under value. Syntax same as enable. Messages 16 | # specified are enabled, while categories only check already-enabled messages. 17 | fail-on= 18 | 19 | # Specify a score threshold to be exceeded before program exits with error. 20 | fail-under=9.9 21 | 22 | # Files or directories to be skipped. They should be base names, not paths. 23 | ignore=CVS 24 | 25 | # Add files or directories matching the regex patterns to the ignore-list. The 26 | # regex matches against paths. 27 | ignore-paths= 28 | 29 | # Files or directories matching the regex patterns are skipped. The regex 30 | # matches against base names, not paths. 31 | ignore-patterns= 32 | 33 | # Python code to execute, usually for sys.path manipulation such as 34 | # pygtk.require(). 35 | #init-hook= 36 | 37 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 38 | # number of processors available to use. 39 | jobs=1 40 | 41 | # Control the amount of potential inferred values when inferring a single 42 | # object. This can help the performance when dealing with large functions or 43 | # complex, nested conditions. 44 | limit-inference-results=100 45 | 46 | # List of plugins (as comma separated values of python module names) to load, 47 | # usually to register additional checkers. 48 | load-plugins= 49 | 50 | # Pickle collected data for later comparisons. 51 | persistent=yes 52 | 53 | # When enabled, pylint would attempt to guess common misconfiguration and emit 54 | # user-friendly hints instead of false-positive error messages. 55 | suggestion-mode=yes 56 | 57 | # Allow loading of arbitrary C extensions. Extensions are imported into the 58 | # active Python interpreter and may run arbitrary code. 59 | unsafe-load-any-extension=no 60 | 61 | 62 | [MESSAGES CONTROL] 63 | 64 | # Only show warnings with the listed confidence levels. Leave empty to show 65 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 66 | confidence= 67 | 68 | # Disable the message, report, category or checker with the given id(s). You 69 | # can either give multiple identifiers separated by comma (,) or put this 70 | # option multiple times (only on the command line, not in the configuration 71 | # file where it should appear only once). You can also use "--disable=all" to 72 | # disable everything first and then reenable specific checks. For example, if 73 | # you want to run only the similarities checker, you can use "--disable=all 74 | # --enable=similarities". If you want to run only the classes checker, but have 75 | # no Warning level messages displayed, use "--disable=all --enable=classes 76 | # --disable=W". 77 | disable=logging-fstring-interpolation, 78 | suppressed-message, 79 | useless-suppression, 80 | deprecated-pragma, 81 | use-symbolic-message-instead 82 | 83 | # Enable the message, report, category or checker with the given id(s). You can 84 | # either give multiple identifier separated by comma (,) or put this option 85 | # multiple time (only on the command line, not in the configuration file where 86 | # it should appear only once). See also the "--disable" option for examples. 87 | enable=c-extension-no-member 88 | 89 | 90 | [REPORTS] 91 | 92 | # Python expression which should return a score less than or equal to 10. You 93 | # have access to the variables 'error', 'warning', 'refactor', and 'convention' 94 | # which contain the number of messages in each category, as well as 'statement' 95 | # which is the total number of statements analyzed. This score is used by the 96 | # global evaluation report (RP0004). 97 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 98 | 99 | # Template used to display messages. This is a python new-style format string 100 | # used to format the message information. See doc for all details. 101 | #msg-template= 102 | 103 | # Set the output format. Available formats are text, parseable, colorized, json 104 | # and msvs (visual studio). You can also give a reporter class, e.g. 105 | # mypackage.mymodule.MyReporterClass. 106 | output-format=text 107 | 108 | # Tells whether to display a full report or only the messages. 109 | reports=no 110 | 111 | # Activate the evaluation score. 112 | score=yes 113 | 114 | 115 | [REFACTORING] 116 | 117 | # Maximum number of nested blocks for function / method body 118 | max-nested-blocks=5 119 | 120 | # Complete name of functions that never returns. When checking for 121 | # inconsistent-return-statements if a never returning function is called then 122 | # it will be considered as an explicit return statement and no message will be 123 | # printed. 124 | never-returning-functions=sys.exit,argparse.parse_error 125 | 126 | 127 | [VARIABLES] 128 | 129 | # List of additional names supposed to be defined in builtins. Remember that 130 | # you should avoid defining new builtins when possible. 131 | additional-builtins= 132 | 133 | # Tells whether unused global variables should be treated as a violation. 134 | allow-global-unused-variables=yes 135 | 136 | # List of names allowed to shadow builtins 137 | allowed-redefined-builtins= 138 | 139 | # List of strings which can identify a callback function by name. A callback 140 | # name must start or end with one of those strings. 141 | callbacks=cb_, 142 | _cb 143 | 144 | # A regular expression matching the name of dummy variables (i.e. expected to 145 | # not be used). 146 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 147 | 148 | # Argument names that match this expression will be ignored. Default to name 149 | # with leading underscore. 150 | ignored-argument-names=_.*|^ignored_|^unused_ 151 | 152 | # Tells whether we should check for unused import in __init__ files. 153 | init-import=no 154 | 155 | # List of qualified module names which can have objects that can redefine 156 | # builtins. 157 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 158 | 159 | 160 | [SPELLING] 161 | 162 | # Limits count of emitted suggestions for spelling mistakes. 163 | max-spelling-suggestions=4 164 | 165 | # Spelling dictionary name. Available dictionaries: none. To make it work, 166 | # install the 'python-enchant' package. 167 | spelling-dict= 168 | 169 | # List of comma separated words that should be considered directives if they 170 | # appear and the beginning of a comment and should not be checked. 171 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: 172 | 173 | # List of comma separated words that should not be checked. 174 | spelling-ignore-words= 175 | 176 | # A path to a file that contains the private dictionary; one word per line. 177 | spelling-private-dict-file= 178 | 179 | # Tells whether to store unknown words to the private dictionary (see the 180 | # --spelling-private-dict-file option) instead of raising a message. 181 | spelling-store-unknown-words=no 182 | 183 | 184 | [STRING] 185 | 186 | # This flag controls whether inconsistent-quotes generates a warning when the 187 | # character used as a quote delimiter is used inconsistently within a module. 188 | check-quote-consistency=no 189 | 190 | # This flag controls whether the implicit-str-concat should generate a warning 191 | # on implicit string concatenation in sequences defined over several lines. 192 | check-str-concat-over-line-jumps=no 193 | 194 | 195 | [BASIC] 196 | 197 | # Naming style matching correct argument names. 198 | argument-naming-style=snake_case 199 | 200 | # Regular expression matching correct argument names. Overrides argument- 201 | # naming-style. 202 | #argument-rgx= 203 | 204 | # Naming style matching correct attribute names. 205 | attr-naming-style=snake_case 206 | 207 | # Regular expression matching correct attribute names. Overrides attr-naming- 208 | # style. 209 | #attr-rgx= 210 | 211 | # Bad variable names which should always be refused, separated by a comma. 212 | bad-names=foo, 213 | bar, 214 | baz, 215 | toto, 216 | tutu, 217 | tata 218 | 219 | # Bad variable names regexes, separated by a comma. If names match any regex, 220 | # they will always be refused 221 | bad-names-rgxs= 222 | 223 | # Naming style matching correct class attribute names. 224 | class-attribute-naming-style=any 225 | 226 | # Regular expression matching correct class attribute names. Overrides class- 227 | # attribute-naming-style. 228 | #class-attribute-rgx= 229 | 230 | # Naming style matching correct class constant names. 231 | class-const-naming-style=UPPER_CASE 232 | 233 | # Regular expression matching correct class constant names. Overrides class- 234 | # const-naming-style. 235 | #class-const-rgx= 236 | 237 | # Naming style matching correct class names. 238 | class-naming-style=PascalCase 239 | 240 | # Regular expression matching correct class names. Overrides class-naming- 241 | # style. 242 | #class-rgx= 243 | 244 | # Naming style matching correct constant names. 245 | const-naming-style=UPPER_CASE 246 | 247 | # Regular expression matching correct constant names. Overrides const-naming- 248 | # style. 249 | #const-rgx= 250 | 251 | # Minimum line length for functions/classes that require docstrings, shorter 252 | # ones are exempt. 253 | docstring-min-length=-1 254 | 255 | # Naming style matching correct function names. 256 | function-naming-style=snake_case 257 | 258 | # Regular expression matching correct function names. Overrides function- 259 | # naming-style. 260 | #function-rgx= 261 | 262 | # Good variable names which should always be accepted, separated by a comma. 263 | good-names=i, 264 | j, 265 | k, 266 | ex, 267 | Run, 268 | df, 269 | _ 270 | 271 | # Good variable names regexes, separated by a comma. If names match any regex, 272 | # they will always be accepted 273 | good-names-rgxs= 274 | 275 | # Include a hint for the correct naming format with invalid-name. 276 | include-naming-hint=no 277 | 278 | # Naming style matching correct inline iteration names. 279 | inlinevar-naming-style=any 280 | 281 | # Regular expression matching correct inline iteration names. Overrides 282 | # inlinevar-naming-style. 283 | #inlinevar-rgx= 284 | 285 | # Naming style matching correct method names. 286 | method-naming-style=snake_case 287 | 288 | # Regular expression matching correct method names. Overrides method-naming- 289 | # style. 290 | #method-rgx= 291 | 292 | # Naming style matching correct module names. 293 | module-naming-style=snake_case 294 | 295 | # Regular expression matching correct module names. Overrides module-naming- 296 | # style. 297 | #module-rgx= 298 | 299 | # Colon-delimited sets of names that determine each other's naming style when 300 | # the name regexes allow several styles. 301 | name-group= 302 | 303 | # Regular expression which should only match function or class names that do 304 | # not require a docstring. 305 | no-docstring-rgx=^_ 306 | 307 | # List of decorators that produce properties, such as abc.abstractproperty. Add 308 | # to this list to register other decorators that produce valid properties. 309 | # These decorators are taken in consideration only for invalid-name. 310 | property-classes=abc.abstractproperty 311 | 312 | # Naming style matching correct variable names. 313 | variable-naming-style=snake_case 314 | 315 | # Regular expression matching correct variable names. Overrides variable- 316 | # naming-style. 317 | #variable-rgx= 318 | 319 | 320 | [LOGGING] 321 | 322 | # The type of string formatting that logging methods do. `old` means using % 323 | # formatting, `new` is for `{}` formatting. 324 | logging-format-style=old 325 | 326 | # Logging modules to check that the string format arguments are in logging 327 | # function parameter format. 328 | logging-modules=logging 329 | 330 | 331 | [SIMILARITIES] 332 | 333 | # Comments are removed from the similarity computation 334 | ignore-comments=yes 335 | 336 | # Docstrings are removed from the similarity computation 337 | ignore-docstrings=yes 338 | 339 | # Imports are removed from the similarity computation 340 | ignore-imports=no 341 | 342 | # Signatures are removed from the similarity computation 343 | ignore-signatures=no 344 | 345 | # Minimum lines number of a similarity. 346 | min-similarity-lines=4 347 | 348 | 349 | [MISCELLANEOUS] 350 | 351 | # List of note tags to take in consideration, separated by a comma. 352 | notes=FIXME, 353 | XXX, 354 | TODO 355 | 356 | # Regular expression of note tags to take in consideration. 357 | #notes-rgx= 358 | 359 | 360 | [TYPECHECK] 361 | 362 | # List of decorators that produce context managers, such as 363 | # contextlib.contextmanager. Add to this list to register other decorators that 364 | # produce valid context managers. 365 | contextmanager-decorators=contextlib.contextmanager 366 | 367 | # List of members which are set dynamically and missed by pylint inference 368 | # system, and so shouldn't trigger E1101 when accessed. Python regular 369 | # expressions are accepted. 370 | generated-members= 371 | 372 | # Tells whether missing members accessed in mixin class should be ignored. A 373 | # mixin class is detected if its name ends with "mixin" (case insensitive). 374 | ignore-mixin-members=yes 375 | 376 | # Tells whether to warn about missing members when the owner of the attribute 377 | # is inferred to be None. 378 | ignore-none=yes 379 | 380 | # This flag controls whether pylint should warn about no-member and similar 381 | # checks whenever an opaque object is returned when inferring. The inference 382 | # can return multiple potential results while evaluating a Python object, but 383 | # some branches might not be evaluated, which results in partial inference. In 384 | # that case, it might be useful to still emit no-member and other checks for 385 | # the rest of the inferred objects. 386 | ignore-on-opaque-inference=yes 387 | 388 | # List of class names for which member attributes should not be checked (useful 389 | # for classes with dynamically set attributes). This supports the use of 390 | # qualified names. 391 | ignored-classes=optparse.Values,thread._local,_thread._local 392 | 393 | # List of module names for which member attributes should not be checked 394 | # (useful for modules/projects where namespaces are manipulated during runtime 395 | # and thus existing member attributes cannot be deduced by static analysis). It 396 | # supports qualified module names, as well as Unix pattern matching. 397 | ignored-modules= 398 | 399 | # Show a hint with possible names when a member name was not found. The aspect 400 | # of finding the hint is based on edit distance. 401 | missing-member-hint=yes 402 | 403 | # The minimum edit distance a name should have in order to be considered a 404 | # similar match for a missing member name. 405 | missing-member-hint-distance=1 406 | 407 | # The total number of similar names that should be taken in consideration when 408 | # showing a hint for a missing member. 409 | missing-member-max-choices=1 410 | 411 | # List of decorators that change the signature of a decorated function. 412 | signature-mutators= 413 | 414 | 415 | [FORMAT] 416 | 417 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 418 | expected-line-ending-format= 419 | 420 | # Regexp for a line that is allowed to be longer than the limit. 421 | ignore-long-lines=^\s*(# )??$ 422 | 423 | # Number of spaces of indent required inside a hanging or continued line. 424 | indent-after-paren=4 425 | 426 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 427 | # tab). 428 | indent-string=' ' 429 | 430 | # Maximum number of characters on a single line. 431 | max-line-length=100 432 | 433 | # Maximum number of lines in a module. 434 | max-module-lines=2000 435 | 436 | # Allow the body of a class to be on the same line as the declaration if body 437 | # contains single statement. 438 | single-line-class-stmt=no 439 | 440 | # Allow the body of an if to be on the same line as the test if there is no 441 | # else. 442 | single-line-if-stmt=no 443 | 444 | 445 | [DESIGN] 446 | 447 | # List of qualified class names to ignore when countint class parents (see 448 | # R0901) 449 | ignored-parents= 450 | 451 | # Maximum number of arguments for function / method. 452 | max-args=7 453 | max-positional-arguments=6 454 | 455 | # Maximum number of attributes for a class (see R0902). 456 | max-attributes=7 457 | 458 | # Maximum number of boolean expressions in an if statement (see R0916). 459 | max-bool-expr=5 460 | 461 | # Maximum number of branch for function / method body. 462 | max-branches=12 463 | 464 | # Maximum number of locals for function / method body. 465 | max-locals=15 466 | 467 | # Maximum number of parents for a class (see R0901). 468 | max-parents=7 469 | 470 | # Maximum number of public methods for a class (see R0904). 471 | max-public-methods=35 472 | 473 | # Maximum number of return / yield for function / method body. 474 | max-returns=6 475 | 476 | # Maximum number of statements in function / method body. 477 | max-statements=50 478 | 479 | # Minimum number of public methods for a class (see R0903). 480 | min-public-methods=2 481 | 482 | 483 | [IMPORTS] 484 | 485 | # List of modules that can be imported at any level, not just the top level 486 | # one. 487 | allow-any-import-level= 488 | 489 | # Allow wildcard imports from modules that define __all__. 490 | allow-wildcard-with-all=no 491 | 492 | # Analyse import fallback blocks. This can be used to support both Python 2 and 493 | # 3 compatible code, which means that the block might have code that exists 494 | # only in one or another interpreter, leading to false positives when analysed. 495 | analyse-fallback-blocks=no 496 | 497 | # Deprecated modules which should not be used, separated by a comma. 498 | deprecated-modules= 499 | 500 | # Output a graph (.gv or any supported image format) of external dependencies 501 | # to the given file (report RP0402 must not be disabled). 502 | ext-import-graph= 503 | 504 | # Output a graph (.gv or any supported image format) of all (i.e. internal and 505 | # external) dependencies to the given file (report RP0402 must not be 506 | # disabled). 507 | import-graph= 508 | 509 | # Output a graph (.gv or any supported image format) of internal dependencies 510 | # to the given file (report RP0402 must not be disabled). 511 | int-import-graph= 512 | 513 | # Force import order to recognize a module as part of the standard 514 | # compatibility libraries. 515 | known-standard-library= 516 | 517 | # Force import order to recognize a module as part of a third party library. 518 | known-third-party=enchant 519 | 520 | # Couples of modules and preferred modules, separated by a comma. 521 | preferred-modules= 522 | 523 | 524 | [CLASSES] 525 | 526 | # Warn about protected attribute access inside special methods 527 | check-protected-access-in-special-methods=no 528 | 529 | # List of method names used to declare (i.e. assign) instance attributes. 530 | defining-attr-methods=__init__, 531 | __new__, 532 | setUp, 533 | __post_init__ 534 | 535 | # List of member names, which should be excluded from the protected access 536 | # warning. 537 | exclude-protected=_asdict, 538 | _fields, 539 | _replace, 540 | _source, 541 | _make 542 | 543 | # List of valid names for the first argument in a class method. 544 | valid-classmethod-first-arg=cls 545 | 546 | # List of valid names for the first argument in a metaclass class method. 547 | valid-metaclass-classmethod-first-arg=cls 548 | 549 | 550 | [EXCEPTIONS] 551 | 552 | # Exceptions that will emit a warning when being caught. Defaults to 553 | # "BaseException, Exception". 554 | overgeneral-exceptions=builtins.BaseException, 555 | builtins.Exception 556 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.10" 13 | jobs: 14 | pre_build: 15 | - sphinx-apidoc --separate --no-toc --force -o docs/api/ numerapi 16 | 17 | 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | # formats: 20 | # - pdf 21 | 22 | # Build documentation in the docs/ directory with Sphinx 23 | sphinx: 24 | configuration: docs/conf.py 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: docs/requirements.txt 30 | - method: setuptools 31 | path: . 32 | 33 | 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | Notable changes to this project. 3 | 4 | ## [2.20.6] - 2025-04-02 5 | - cli: fix `check_new_round` 6 | 7 | ## [2.20.5] - 2025-04-02 8 | - type hint improvements 9 | - no longer depend on `pkg_resources` 10 | 11 | ## [2.20.1] - 2025-03-29 12 | - add a `py.typed` file to indicate type hints 13 | 14 | ## [2.20.0] - 2025-02-21 15 | - deprecate `round_model_performances` 16 | - support `.env` files for authentication, requires `python-dotenv` 17 | - remove deprecated functionality 18 | - ci: use Python 3.10 19 | 20 | ## [2.19.1] - 2024-07-29 21 | - `stake_drain`: set drain flag correctly, so things will look less confusing 22 | in the UI 23 | - fix `SignalsAPI.upload_predictions` 24 | 25 | ## [2.19.0] - 2024-07-26 - hello CryptoAPI 26 | - fix `SignalsAPI.ticker_universe` 27 | - added `validationBmcMean` and `era.validationBmc` to diagnostics 28 | - added `CyptoAPI` 29 | 30 | ## [2.18.0] - 2024-03-08 31 | - added `get_account_leaderboard` to fetch account-level leaderboard 32 | - added 'models_of_account' to fetch all models of an account 33 | - 'SignalsAPI.ticker_universe': stop using hardcoded URL 34 | - remove `set_stake_type` - no longer relevant 35 | - deprecate 'SignalsAPI.download_validation_data' 36 | - code style fixes 37 | - fix docs 38 | 39 | ## [2.17.0] - 2024-02-18 40 | - signals: add 'list_datasets' and 'download_dataset' 41 | 42 | ## [2.16.3] - 2024-01-01 43 | - add `bmcRep` and `mmcRep` to `get_leaderboard` 44 | 45 | ## [2.16.2] - 2023-12-28 46 | - fix `get_leaderboard` 47 | 48 | ## [2.16.1] - 2023-10-09 49 | - added 'corr20V2Rep' to 'get_leaderboard' 50 | - deprecate methods related to v2 and v3 datasets 51 | 52 | ## [2.16.0] - 2023-09-28 - submission downloads 53 | - added `download_submission` to download a CSV of a previous submission 54 | - added `submission_ids` to fetch all submission_ids of a model 55 | - minor fixes 56 | 57 | ## [2.15.1] - 2023-08-25 - model upload improvements 58 | - model_upload now allows specifying which data_version and which image to use 59 | - added `round_model_performances_v2`, which allows fetching newer metrics 60 | - added `pipeline_status` to get the status of numerai's scoring pipeline 61 | - added `intra_round_scores` to fetch intra-round metrics of your models 62 | - handle downtimes of Numerai's API more gracefully 63 | - added `model_upload_docker_images` and `model_upload_data_versions` 64 | 65 | ## [2.15.0] - 2023-07-13 - model uploads! 66 | - add corr20V2 into round_model_performances (PR #100) 67 | - add tcMultiplier into round_model_performances 68 | - add `model_upload` feature, to allow uploading pickled models to numerai 69 | 70 | ## [2.14.0] - 2023-04-06 - no more submission_status 71 | - deprecate `SignalsAPI.submission_status` 72 | - deprecate `NumerAPI.submission_status` 73 | 74 | ## [2.13.4] - 2023-04-03 75 | - return filepath from `download_dataset` 76 | 77 | ## [2.13.3] - 2023-04-03 78 | - add `modelid_to_modelname` method 79 | - fix `stake_set` 80 | - add option to adjust connection timeout when upload predictionions (PR #97) 81 | - remove deprecated `download_latest_data` 82 | - add `set_global_data_dir` method 83 | - docs 84 | 85 | ## [2.13.2] - 2023-02-22 86 | - add `data_datestamp` argument to `upload_predictions` - this will allow 87 | submitting predictions using data from a previous round. 88 | 89 | ## [2.13.1] - 2023-02-15 90 | - fix `SignalsAPI.daily_model_performances` 91 | - adding all the new metrics (RIC, CorrV4, FncV4, etc) to `diagnostics` 92 | 93 | ## [2.13.0] - 2022-12-16 - "API deprecations" 94 | - `stake_get` - updated to migrate away from deprecated API endpoint 95 | - remove deprecated `SignalsAPI.daily_user_performances` 96 | - remove deprecated `SignalsAPI.daily_submissions_performances` 97 | - remove `round_details` - no longer supported by Numerai 98 | - remove deprecated `daily_submissions_performances` 99 | 100 | ## [2.12.9] - 2022-12-02 101 | - numerapi-cli: make `check-new-round` more robust 102 | - more robust `check_new_round` and `check_round_open` - working around some 103 | edge cases, that occur during the time without any active round. 104 | 105 | ## [2.12.8] - 2022-11-21 106 | - change default argument for `check_new_round` to 12 hours 107 | - deprecate `daily_submissions_performances` 108 | - update `get_leaderboard` to reflect changes in the backend 109 | - add downloads per month batch 110 | - fix docs 111 | - update README 112 | 113 | ## [2.12.7] - 2022-11-02 114 | - fix `check_round_open` 115 | 116 | ## [2.12.6] - 2022-11-01 117 | - add `check_round_open` to check if there is a currently an active round 118 | - make `check_new_round` accessible from `SignalsAPI` 119 | 120 | ## [2.12.5] - 2022-10-24 121 | - add fncV3 to `daily_submissions_performances` 122 | - add `TC` to `SignalsAPI.get_leaderboard` 123 | - add `TC` and `corr60` to `SignalsAPI.daily_model_performances` 124 | - add `TC` to `SignalsAPI.daily_submissions_performances` 125 | - define timeouts for all web call made with `requests` 126 | 127 | ## [2.12.4] - 2022-08-24 128 | - add `set_stake_type` to change payout mode and multipliers (PR #83) 129 | - add auth to `round_details` query (PR #86) 130 | 131 | ## [2.12.3] - 2022-07-03 132 | - fix directory check in download_dataset in the last update 133 | 134 | ## [2.12.2] - 2022-06-30 135 | - `diagnostics` now returns all diagnostics results, if no diagnostics_id is specified 136 | - `download_dataset`: in case the destination path contains a directory, ensure it exists 137 | 138 | ## [2.12.1] - 2022-06-20 139 | - bugfix, timeout for http requests was accidentally set to 3 seconds. 140 | 141 | ## [2.12.0] - 2022-06-17 142 | - make downloads more robust and prevent broken files by downloading to temporary files 143 | - simple retry mechanism for failed API requests (5xx error codes only) 144 | - Remove submission `version` parameter (PR #75) 145 | 146 | ## [2.11.0] - 2022-03-29 147 | - add tc, fnc, fncV3 to get_leaderboard 148 | - add icRank, icRep to SignalsAPI.get_leaderboard 149 | - add tcRank, tcRep, fncV3Rep and fncV3Rank to daily_model_performances 150 | - add icRep and icRank to SignalsAPI.daily_model_performances 151 | - add tc and tcPercentile to daily_submissions_performances 152 | - make `stake_set` work with multi model accounts 153 | - add tc to the round_model_performances method (PR #74) 154 | - add tcPercentile, ic, icPercentile, fncV3, fncV3Percentile to round_model_performances 155 | 156 | ## [2.10.0] - 2022-02-07 157 | - added `set_bio` to programmatically update the bio field for some model 158 | - added `set_link` to programmatically update the user link field 159 | - enable stake changes for Numerai Signals (#68 Thx @habakan) 160 | - run tests via github actions and disable travis integration 161 | 162 | ## [2.9.4] - 2021-11-14 163 | - cli: fix predictions upload 164 | 165 | ## [2.9.3] - 2021-11-12 166 | - cli: support uploading predictions generated with the new dataset (needs `--new_data`) 167 | - signals: make `round_model_performances` available 168 | 169 | ## [2.9.2] - 2021-10-07 170 | - signals: support upload to diagnostic tool 171 | - cli: added `list-datasets` 172 | - cli: implement downloading of the new dataset 173 | - some code cleanup 174 | 175 | ## [2.9.1] - 2021-09-27 176 | - add query `round_model_performances` (#60) 177 | - fix documentation 178 | - upgrade from `latestSubmission` to `latestSubmissionV2` 179 | - Indicate source `numerapi` when uploading submissions 180 | - Deprecate `get_account_transactions` - data no longer available 181 | - Deprecate `get_transactions` - data no longer available 182 | - Add `wallet_transactions`, fetches all transactions to / from your wallet 183 | - Code style improvements 184 | 185 | ## [2.9.0] - 2021-09-15 186 | - support passing `round_num` to `list_datasets`, to get available files from past rounds 187 | - `download_dataset` no longer requires a destination path, it defaults to the source file name 188 | - `download_dataset` now accepts a `round_num` argument, to download old files 189 | - added `upload_diagnostics` to upload to the new diagnostics tool 190 | - added `diagnostics` to fetch results of a diagnostics run 191 | 192 | ## [2.8.1] - 2021-09-08 193 | - Add version arg to upload_predictions (#59) 194 | 195 | ## [2.8.0] - 2021-09-07 - "new data api" 196 | - added `list_datasets` to fetch the list of available data files 197 | - added `download_dataset` to download files from the new data api 198 | - add missing documentation and deprecation warnings 199 | 200 | ## [2.7.1] - 2021-09-01 201 | - Add mmc20d rep and rank to SignalsAPI daily_model_performances 202 | - rename `corr_20d*` to `corr20d*` since Numerai's GraphQL adapter now handles numbers in fields without underscores 203 | 204 | ## [2.7.0] - 2021-08-27 205 | - adding `fncPercentile`, `mmcPercentile` & `corrPercentile` to `daily_submissions_performances` 206 | - replace deprecated GraphQL `v2UserProfile` call with `v3UserProfile` 207 | - replace deprecated GraphQL `signalsUserProfile` call with `v2SignalsProfile` 208 | - new `daily_model_performances`, replacing `daily_user_performances` 209 | - signals: new `daily_model_performances`, replacing `daily_user_performances` 210 | - update & fix command line interface 211 | 212 | ## [2.6.0] - 2021-07-12 213 | - cli: remove deprecated 'payments' and 'user-activities' commands (#51) 214 | - cli: converting the output to JSON (#52) 215 | 216 | ## [2.5.2] - 2021-06-30 217 | - remove deprecated fields (#50) 218 | - remove userActivities query (deprecated, use userProfile fields instead) 219 | - remove payments query (deprecated, use userProfile fields instead) 220 | - remove misc deprecated fields from userProfile like badges, earnings 221 | - remove misc deprecated scores from submissions like consistency and concordance 222 | - remove misc deprecated fields from userProfile.dailyUserPerformances like reputation and rolling_score_rep (use corrRep/mmcRep/fncRep instead), and all the early staking 2.0 fields like averageCorrelation, averageCorrelationPayout, sumDeltaCorrelation etc. 223 | - fix 'get_competitions' by removig deprecated fields (#49) 224 | 225 | ## [2.5.1] - 2021-05-10 226 | - lower pandas requirement to pandas>=1.1.0 to fix problems for users working in google colab (#48) 227 | 228 | ## [2.5.0] - 2021-05-09 229 | - resumable download (#42) 230 | - Upload submission functions using a pandas dataframe (#46) 231 | 232 | ## [2.4.5] - 2021-03-18 233 | - enable registering submission webhooks and trigger IDs (#44) 234 | 235 | ## [2.4.4] - 2021-03-04 236 | - make `get_current_round` available in SignalsAPI 237 | - add `download_validation_data` to SignalsAPI, to download the latest validation 238 | data, historical targets and ticker universe 239 | 240 | ## [2.4.3] - 2021-02-27 241 | - deprecate multi-tournament handling 242 | - `get_models` now returns the list of models depending on the tournament you 243 | are working on - numerai classic (NumerAPI) vs numerai signals (SignalsAPI). 244 | This is necessary after the recent "model split". 245 | 246 | ## [2.4.2] - 2021-02-25 247 | - `daily_user_performances` add `fnc` 248 | - `daily_submissions_performances` filter all-None items 249 | 250 | ## [2.4.1] - 2021-02-13 251 | - improve docstrings and signals example code 252 | - remove deprecated `get_v1_leaderboard` 253 | - remove deprecated `get_stakes` & `get_submission_ids` 254 | - Fix default file path for `download_latest_data` (#37) 255 | - test suite: fixes 256 | 257 | ## [2.4.0] - 2021-01-12 258 | - fix `stake_change` call by adding `tournament` parameter (#32) 259 | - add `tournament` parameter to all stake related endpoints 260 | - code style checks with `flake8` 261 | - Remove header from signals universe (#33) 262 | - Add `get_latest_data_path` and `download_latest_data` (#35) 263 | 264 | ## [2.3.9] - 2020-11-26 265 | - Add additional metrics to `submission_status` (#30) 266 | - signals: add `mmc`, `mmcRank` and `nmrStaked` to `get_leaderboard` 267 | - signals: add `totalStake` to `public_user_profile` 268 | - signals: add `stake_get` 269 | 270 | ## [2.3.8] - 2020-10-27 271 | - signals: speedup `ticker_universe` 272 | - signals: add `mmcRep` and `reputation` to `daily_user_performances` 273 | - signals: add `mmc`, `mmcRep`, `correlation`, `corrRep` and `roundNumber` 274 | to `daily_submissions_performances` 275 | 276 | ## [2.3.7] - 2020-10-15 277 | - signals: fix ticker universe 278 | 279 | ## [2.3.6] - 2020-10-07 280 | - signals: update ticker universe path (#29) 281 | 282 | ## [2.3.5] - 2020-09-28 283 | - Add signals diagnostics (#28) 284 | 285 | ## [2.3.4] - 2020-08-10 286 | - update 'ticker_universe' to use the update file location 287 | 288 | ## [2.3.3] - 2020-07-22 289 | - get Numerai compute id if available and pass it along during predictions upload 290 | 291 | ## [2.3.2] - 2020-07-21 292 | - Signals: added `ticker_universe` to get the list of accepted tickers 293 | - `submission_status` no longer needs (and accepts) a submission_id. It 294 | automatically uses the last submission associated with a model 295 | 296 | ## [2.3.1] - 2020-05-06 - "Signals" 297 | - fix Signals submission upload (#25) 298 | 299 | ## [2.3.0] - 2020-05-06 - "Signals" 300 | - added API for Numerai Signals 301 | - refactor codebase 302 | - more tests 303 | 304 | ## [2.2.4] - 2020-05-11 305 | - Remove required model_id annotation for submissions status lookups so that None can be passed 306 | - Use consistent modelId in query spec 307 | - Update doc examples 308 | 309 | ## [2.2.2] - 2020-05-09 310 | - fix `submission_status` for multi model accounts 311 | 312 | ## [2.2.0] - 2020-04-17 313 | - no more Python2 support 314 | - added type hints 315 | - add `get_account` to return private account information and deprecates `get_user` (#23) 316 | - incorporates updates to the Numerai tournament API in anticipation of the rollout of a new account system with multi-model support (#23) 317 | 318 | ## [2.1.6] - 2020-04-08 319 | - add `rolling_score_rep` to `daily_user_performances` and `get_leaderboard` 320 | - deprecate `reputation` in `daily_user_performances` and `get_leaderboard` 321 | 322 | ## [2.1.5] - 2020-04-03 323 | - added `payoutPending` and `payoutSettled` to `get_leaderboard` (#21) 324 | - added `sumDeltaCorrelation`, `finalCorrelation`, `payoutPending` and `payoutSettled` to `daily_user_performances` (#21) 325 | 326 | ## [2.1.4] - 2020-03-30 - "Spring cleanup" 327 | - added "sharpe", "feature exposure" and "correlation with example predictions" to `submission_status` 328 | - remove deprecated `check_submission_successful` 329 | - added `bio` and `totalStake` to `public_user_profile` 330 | - remove deprecated `get_rankings` 331 | 332 | ## [2.1.3] - 2020-03-30 333 | - fix `get_user_activities` 334 | - remove deprecated `get_staking_leaderboard`, `get_nmr_prize_pool` 335 | - added `mmc` and `correlationWithMetamodel` to `daily_submissions_performances` 336 | 337 | ## [2.1.2] - 2019-11-30 338 | - fix staking after recent changes to the GraphQL backend 339 | 340 | ## [2.1.1] - 2019-11-23 341 | - add `round_details`, returning correlation scores of all users for the round 342 | 343 | ## [2.1.0] - 2019-11-15 344 | - add some more details to `get_leaderboard` 345 | - adapt to changes in Numerai's staking API 346 | 347 | ## [2.0.1] - 2019-10-28 348 | - fix `stake_set` 349 | 350 | ## [2.0.0] - 2019-10-23 351 | - add v2 version of `get_leaderboard` 352 | - add `stake_get` & `stake_set` 353 | - add `stake_increase`, `stake_decrease` & `stake_drain` 354 | - add `public_user_profile` 355 | - add `daily_user_performances` 356 | - add `daily_submissions_performances` 357 | - remove v1 staking 358 | - remove `get_staking_cutoff` - no longer relevant 359 | - old `get_leaderboard` renamed to `get_v1_leaderboard` 360 | - add v2-style staking to cli interface 361 | - update documentation 362 | 363 | ## [1.6.2] - 2019-07-31 364 | - remove phone number and bonus fetching (#16) 365 | 366 | ## [1.6.1] - 2019-07-12 367 | - fix downloading dataset for tournaments > 1 368 | - add `validationCorrelation` and `liveCorrelation` to all relevant places 369 | - remove `validationAuroc` and `validationLogloss` from `submission_status` 370 | 371 | ## [1.6.0] - 2019-07-10 372 | - default to tournament 8 `katzuagi` 373 | - update docstring 374 | - added `reputationPayments`, 375 | 376 | ## [1.5.5] - 2019-06-13 377 | - include `otherUsdIssuances` and `phoneVerificationBonus` to `get_payments` 378 | - add datetime information (`insertedAt`) to `get_transactions` 379 | 380 | ## [1.5.4] - 2019-05-30 381 | - return new `reputation` as announced by numerai on 2019-05-29 in `get_rankings` 382 | 383 | ## [1.5.3] - 2019-05-23 384 | - fix setup.py to make it work with the latest twine version 385 | 386 | ## [1.5.2] - 2019-05-22 387 | - add NMR returned information to `get_leaderboard` - useful for partial burns 388 | 389 | ## [1.5.1] - 2019-04-14 390 | - fix `get_staking_cutoff` for rounds >= 154 391 | 392 | ## [1.5.0] - 2019-04-03 393 | - tests: start testing the cli interface 394 | - cli: fix `version` command on Python2.7 395 | - added `liveAuroc` and `validationAuroc` to `get_leaderboard` 396 | - added `liveAuroc` and `validationAuroc` to `get_staking_leaderboard` 397 | - added `liveAuroc` and `validationAuroc` to `get_user_activities` 398 | - added `validationAuroc` to `submission_status` 399 | - added `ruleset` to `get_competitions` 400 | - added `phoneNumber` and `country` to `get_user` 401 | - remove consistency check from `test_check_submission_successful` 402 | 403 | ## [1.4.6] - 2019-03-30 404 | - remove total payments from leaderboard query (#13) 405 | - fix `get_staking_leaderboard` 406 | 407 | ## [1.4.5] - 2019-03-05 408 | - `get_tournaments` now allows to filter for active tournaments only 409 | - CLI: `tournaments` gained `active_only` / `all` flags to get all or only 410 | the active tournaments 411 | 412 | ## [1.4.4] - 2019-02-17 413 | - remove timeout completely to fix upload issues 414 | 415 | ## [1.4.3] - 2019-02-17 416 | - increase default timeout to 20s 417 | - better error handling 418 | 419 | ## [1.4.2] - 2019-02-10 420 | - `get_staking_cutoff` now gets the cutoff values via the api, instead of 421 | doing it's own computation 422 | - compatibility with `click` version >= 7.0 423 | 424 | ## [1.4.1] - 2019-02-10 425 | - handle connection errors more gracefully (#11) 426 | - pin minimum version of tqdm to (hopefully) prevent an exception (#12) 427 | - travis: test against Python 3.7 428 | 429 | ## [1.4.0] - 2018-11-16 430 | - added `burned` to `get_user_activities` 431 | - docs: fixed typos + improved example 432 | - `validation_logloss` -> `validationLogloss`, to follow numerai's docs 433 | - remove everything `originality` related 434 | 435 | ## [1.3.0] - 2018-08-09 436 | - added `get_staking_cutoff` to compute staking cutoff for a given round and tournament. 437 | - added `get_nmr_prize_pool` to get the NMR prize pool for a given round and tournament. 438 | 439 | ## [1.2.1] - 2018-08-05 440 | - removed `filename` from `get_user_activities`, no longer supported. 441 | - rename `get_submission_filename` to `get_submission_filenames` 442 | - `get_submission_filenames` now only works for the authorized user. It allows 443 | to get ones submission filenames, optionally filtered by round_num and 444 | tournament. 445 | 446 | ## [1.2.0] - 2018-08-03 447 | - added `get_rankings`, which gives access to numerai's global leaderboard 448 | - added `get_user_activities`, that allows to see each user's submission and 449 | staking activity 450 | - added `get_submission_filename` to get the submission filename for any user, 451 | tournament & round number combination 452 | - added `prizePoolNmr`, `prizePoolUsd` and number of `participants` to the 453 | `get_competitions` endpoint 454 | - ensure functionality of command line interface is in sync 455 | 456 | ## [1.1.1] - 2018-06-06 457 | - added `get_tournaments` 458 | - added `tournament_name2number` and `tournament_number2name` to translate 459 | between tournament numbers and names 460 | 461 | ## [1.1.0] - 2018-05-24 462 | - added numerapi command line interface 463 | - allow passing public ID and secret key via environment variables 464 | 465 | ## [1.0.1] - 2018-05-17 466 | - added `stakeResolution` information to get_leaderboard 467 | - added badge for read the docs to README 468 | 469 | ## [1.0.0] - 2018-04-25 470 | - publish README as long_description on pypi 471 | - fixed `get_transactions` after API change on Numerai's side 472 | - added proper docstrings to all public methods, using Google Style as 473 | described at http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html 474 | - added examples for all public methods 475 | - added documentation on readthedocs: http://numerapi.readthedocs.io 476 | 477 | ## [0.9.1] - 2018-04-22 478 | - add tournamentId to `get_stakes` 479 | - fixed `stake` after API change on Numerai's side 480 | 481 | ## [0.9.0] - 2018-04-13 482 | - support tournament parameter for various endpoints. Numer.ai is planning to 483 | run more than one tournament at a time. This change makes numerapi ready for 484 | that. 485 | - minor code cleanup 486 | 487 | ## [0.8.3] - 2018-04-07 488 | - don't query Numerai's API if the action requires an auth token, but there 489 | is none provided 490 | - more & improved tests (test coverage now > 90%) 491 | - consistency threshold moved to 58, following the latest rule change 492 | 493 | ## [0.8.2] - 2018-03-09 494 | - use `decimal.Decimal` instead of floats to avoid rounding errors (#3) 495 | - optional flag to turn of tqdm's progress bars (#4) 496 | - update `check_submission_successful` to recent rule changes (originality 497 | no longer required) 498 | - update documentation 499 | 500 | ## [0.8.1] - 2018-01-27 501 | - import NumerAPI class to toplevel. now `from numerapi import NumerAPI` works 502 | - added `get_dataset_url` 503 | - more & improved tests 504 | 505 | ## [0.8.0] - 2018-01-06 506 | - added `check_new_round` to check if a new round has started 507 | - added `check_submission_successful` to check if the last submission passes 508 | concordance, originality and consistency 509 | - return proper Python data types, for example the NMR amounts are now 510 | floats and no longer strings 511 | - show progress bar while downloading dataset 512 | - general code cleanup & more tests 513 | 514 | ## [0.7.1] - 2017-12-29 515 | - fix import issues (py2 vs py3) 516 | 517 | ## [0.7.0] - 2017-12-29 518 | - convert datetime strings to proper Python datetime objects 519 | - only append .zip to downloaded dataset if zip=True 520 | - use round_number instead of date in default download filename 521 | - setup travis to run test automatically 522 | - run tests with different Python versions (2.7, 3.5 and 3.6) 523 | - test coverage reports via codecov.io 524 | 525 | ## [0.6.3] - 2017-12-20 526 | - complete rewrite to adapt to Numerai's API switch to GraphQL 527 | - update documentation and example 528 | - added staking via API - `stake` 529 | - added `get_staking_leaderboard` 530 | - allow passing desired filename to data download 531 | - allow custom API calls - `raw_query` 532 | - started a test suite 533 | - moved numerapi to it's new home (https://github.com/uuazed/numerapi) 534 | - make numerapi available on pypi (https://pypi.org/project/numerapi) 535 | - rename package from NumerAPI to all-lowercase numerapi 536 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://app.travis-ci.com/uuazed/numerapi.svg)](https://app.travis-ci.com/uuazed/numerapi) 2 | [![codecov](https://codecov.io/gh/uuazed/numerapi/branch/master/graph/badge.svg)](https://codecov.io/gh/uuazed/numerapi) 3 | [![PyPI](https://img.shields.io/pypi/v/numerapi.svg)](https://pypi.python.org/pypi/numerapi) 4 | [![Downloads](https://pepy.tech/badge/numerapi/month)](https://pepy.tech/project/numerapi) 5 | [![Docs](https://readthedocs.org/projects/numerapi/badge/?version=stable)](http://numerapi.readthedocs.io/en/stable/?badge=stable) 6 | 7 | # Numerai Python API 8 | Automatically download and upload data for the Numerai machine learning 9 | competition. 10 | 11 | This library is a Python client to the Numerai API. The interface is programmed 12 | in Python and allows downloading the training data, uploading predictions, and 13 | accessing user, submission and competitions information. It works for both, the 14 | main competition and the newer Numerai Signals competition. 15 | 16 | If you encounter a problem or have suggestions, feel free to open an issue. 17 | 18 | # Installation 19 | `pip install --upgrade numerapi` 20 | 21 | # Usage 22 | 23 | Numerapi can be used as a regular, importable Python module or from the command 24 | line. 25 | 26 | Some actions (like uploading predictions or staking) require a token to verify 27 | that it is really you interacting with Numerai's API. These tokens consists of 28 | a `public_id` and `secret_key`. Both can be obtained by login in to Numer.ai and 29 | going to Account -> Custom API Keys. Tokens can be passed to the Python module 30 | as parameters or you can be set via environment variables (`NUMERAI_PUBLIC_ID` 31 | and `NUMERAI_SECRET_KEY`). 32 | 33 | ## Python module 34 | 35 | ### Usage example - main competition 36 | 37 | import numerapi 38 | # some API calls do not require logging in 39 | napi = numerapi.NumerAPI(verbosity="info") 40 | # download current dataset => also check `https://numer.ai/data` 41 | napi.download_dataset("v4/train.parquet", "train.parquet") 42 | # get current leaderboard 43 | leaderboard = napi.get_leaderboard() 44 | # check if a new round has started 45 | if napi.check_new_round(): 46 | print("new round has started within the last 12hours!") 47 | else: 48 | print("no new round within the last 12 hours") 49 | 50 | # provide api tokens 51 | example_public_id = "somepublicid" 52 | example_secret_key = "somesecretkey" 53 | napi = numerapi.NumerAPI(example_public_id, example_secret_key) 54 | 55 | # upload predictions 56 | model_id = napi.get_models()['uuazed'] 57 | napi.upload_predictions("preds.csv", model_id=model_id) 58 | # increase your stake by 1.2 NMR 59 | napi.stake_increase(1.2) 60 | 61 | # convert results to a pandas dataframe 62 | import pandas as pd 63 | df = pd.DataFrame(napi.daily_user_performances("uuazed")) 64 | 65 | 66 | ### Usage example - Numerai Signals 67 | 68 | import numerapi 69 | 70 | napi = numerapi.SignalsAPI() 71 | # get current leaderboard 72 | leaderboard = napi.get_leaderboard() 73 | 74 | # setup API with api tokens 75 | example_public_id = "somepublicid" 76 | example_secret_key = "somesecretkey" 77 | napi = numerapi.SignalsAPI(example_public_id, example_secret_key) 78 | 79 | # upload predictions 80 | model_id = napi.get_models()['uuazed'] 81 | napi.upload_predictions("preds.csv", model_id=model_id) 82 | 83 | # get daily performance as pandas dataframe 84 | import pandas as pd 85 | df = pd.DataFrame(napi.daily_user_performances("uuazed")) 86 | 87 | # using the diagnostics tool 88 | napi.upload_diagnostics("preds.csv", model_id=model_id) 89 | # ... or using a pandas DataFrame directly 90 | napi.upload_diagnostics(df=df, model_id=model_id) 91 | # fetch results 92 | napi.diagnostic(model_id) 93 | 94 | 95 | ## Command line interface 96 | 97 | To get started with the cli interface, let's take a look at the help page: 98 | 99 | $ numerapi --help 100 | Usage: numerapi [OPTIONS] COMMAND [ARGS]... 101 | 102 | Wrapper around the Numerai API 103 | 104 | Options: 105 | --help Show this message and exit. 106 | 107 | Commands: 108 | account Get all information about your account! 109 | check-new-round Check if a new round has started within... 110 | competitions Retrieves information about all... 111 | current-round Get number of the current active round. 112 | daily-model-performances Fetch daily performance of a model. 113 | daily-submissions-performances Fetch daily performance of a user's... 114 | dataset-url Fetch url of the current dataset. 115 | download-dataset Download specified file for the given... 116 | download-dataset-old Download dataset for the current active... 117 | leaderboard Get the leaderboard. 118 | list-datasets List of available data files 119 | models Get map of account models! 120 | profile Fetch the public profile of a user. 121 | stake-decrease Decrease your stake by `value` NMR. 122 | stake-drain Completely remove your stake. 123 | stake-get Get stake value of a user. 124 | stake-increase Increase your stake by `value` NMR. 125 | submission-filenames Get filenames of your submissions 126 | submit Upload predictions from file. 127 | transactions List all your deposits and withdrawals. 128 | user Get all information about you!... 129 | version Installed numerapi version. 130 | 131 | 132 | Each command has it's own help page, for example: 133 | 134 | $ numerapi submit --help 135 | Usage: numerapi submit [OPTIONS] PATH 136 | 137 | Upload predictions from file. 138 | 139 | Options: 140 | --tournament INTEGER The ID of the tournament, defaults to 1 141 | --model_id TEXT An account model UUID (required for accounts with 142 | multiple models 143 | 144 | --help Show this message and exit. 145 | 146 | 147 | # API Reference 148 | 149 | Checkout the [detailed API docs](http://numerapi.readthedocs.io/en/latest/api/numerapi.html#module-numerapi.numerapi) 150 | to learn about all available methods, parameters and returned values. 151 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = numerapi 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../CHANGELOG.md 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Numerapi documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Feb 19 00:05:47 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import datetime 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # Insert numerapi' path into the system. 25 | sys.path.insert(0, os.path.abspath("..")) 26 | 27 | import numerapi 28 | 29 | 30 | # -- General configuration ------------------------------------------------ 31 | 32 | # If your documentation needs a minimal Sphinx version, state it here. 33 | # needs_sphinx = '1.0' 34 | 35 | # Add any Sphinx extension module names here, as strings. They can be 36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 37 | # ones. 38 | extensions = [ 39 | "sphinx.ext.autodoc", 40 | "sphinx.ext.intersphinx", 41 | "sphinx.ext.todo", 42 | "sphinx.ext.viewcode", 43 | "sphinx.ext.napoleon", 44 | 'sphinx.ext.doctest', 45 | 'm2r' 46 | ] 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ["_templates"] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | source_suffix = ['.rst', '.md'] 54 | 55 | # The encoding of source files. 56 | # source_encoding = 'utf-8-sig' 57 | 58 | # The master toctree document. 59 | master_doc = "index" 60 | 61 | # General information about the project. 62 | project = 'numerapi' 63 | copyright = str(datetime.datetime.now().year) + ', uuazed' 64 | author = 'uuazed' 65 | 66 | # The version info for the project you're documenting, acts as replacement for 67 | # |version| and |release|, also used in various other places throughout the 68 | # built documents. 69 | # 70 | # The short X.Y version. 71 | version = release = numerapi.__version__ 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | # 76 | # This is also used if you do content translation via gettext catalogs. 77 | # Usually you set "language" from the command line for these cases. 78 | language = None 79 | 80 | # There are two options for replacing |today|: either, you set today to some 81 | # non-false value, then it is used: 82 | # today = '' 83 | # Else, today_fmt is used as the format for a strftime call. 84 | # today_fmt = '%B %d, %Y' 85 | 86 | # List of patterns, relative to source directory, that match files and 87 | # directories to ignore when looking for source files. 88 | exclude_patterns = ["_build"] 89 | 90 | # The reST default role (used for this markup: `text`) to use for all 91 | # documents. 92 | # default_role = None 93 | 94 | # If true, '()' will be appended to :func: etc. cross-reference text. 95 | add_function_parentheses = False 96 | 97 | # If true, the current module name will be prepended to all description 98 | # unit titles (such as .. function::). 99 | add_module_names = True 100 | 101 | # If true, sectionauthor and moduleauthor directives will be shown in the 102 | # output. They are ignored by default. 103 | # show_authors = False 104 | 105 | # The name of the Pygments (syntax highlighting) style to use. 106 | pygments_style = "sphinx" 107 | 108 | # A list of ignored prefixes for module index sorting. 109 | # modindex_common_prefix = [] 110 | 111 | # If true, keep warnings as "system message" paragraphs in the built documents. 112 | # keep_warnings = False 113 | 114 | # If true, `todo` and `todoList` produce output, else they produce nothing. 115 | todo_include_todos = True 116 | 117 | 118 | # -- Options for HTML output ---------------------------------------------- 119 | 120 | # The theme to use for HTML and HTML Help pages. See the documentation for 121 | # a list of builtin themes. 122 | html_theme = "alabaster" 123 | 124 | # Theme options are theme-specific and customize the look and feel of a theme 125 | # further. For a list of options available for each theme, see the 126 | # documentation. 127 | html_theme_options = { 128 | "show_powered_by": False, 129 | "github_user": "uuazed", 130 | "github_repo": "numerapi", 131 | "github_banner": True, 132 | "show_related": False, 133 | "note_bg": "#FFF59C", 134 | } 135 | 136 | # Add any paths that contain custom themes here, relative to this directory. 137 | # html_theme_path = [] 138 | 139 | # The name for this set of Sphinx documents. If None, it defaults to 140 | # " v documentation". 141 | # html_title = None 142 | 143 | # A shorter title for the navigation bar. Default is the same as html_title. 144 | # html_short_title = None 145 | 146 | # The name of an image file (relative to this directory) to place at the top 147 | # of the sidebar. 148 | # html_logo = None 149 | 150 | # The name of an image file (within the static path) to use as favicon of the 151 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 152 | # pixels large. 153 | # html_favicon = None 154 | 155 | # Add any paths that contain custom static files (such as style sheets) here, 156 | # relative to this directory. They are copied after the builtin static files, 157 | # so a file named "default.css" will overwrite the builtin "default.css". 158 | html_static_path = ["_static"] 159 | 160 | # Add any extra paths that contain custom files (such as robots.txt or 161 | # .htaccess) here, relative to this directory. These files are copied 162 | # directly to the root of the documentation. 163 | # html_extra_path = [] 164 | 165 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 166 | # using the given strftime format. 167 | # html_last_updated_fmt = '%b %d, %Y' 168 | 169 | # If true, SmartyPants will be used to convert quotes and dashes to 170 | # typographically correct entities. 171 | html_use_smartypants = False 172 | 173 | # Custom sidebar templates, maps document names to template names. 174 | html_sidebars = { 175 | '**': [ 176 | 'relations.html', # needs 'show_related': True theme option to display 177 | 'searchbox.html', 178 | ] 179 | } 180 | 181 | # Additional templates that should be rendered to pages, maps page names to 182 | # template names. 183 | # html_additional_pages = {} 184 | 185 | # If false, no module index is generated. 186 | # html_domain_indices = True 187 | 188 | # If false, no index is generated. 189 | # html_use_index = True 190 | 191 | # If true, the index is split into individual pages for each letter. 192 | # html_split_index = False 193 | 194 | # If true, links to the reST sources are added to the pages. 195 | html_show_sourcelink = False 196 | 197 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 198 | html_show_sphinx = False 199 | 200 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 201 | html_show_copyright = True 202 | 203 | # If true, an OpenSearch description file will be output, and all pages will 204 | # contain a tag referring to it. The value of this option must be the 205 | # base URL from which the finished HTML is served. 206 | # html_use_opensearch = '' 207 | 208 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 209 | # html_file_suffix = None 210 | 211 | # Language to be used for generating the HTML full-text search index. 212 | # Sphinx supports the following languages: 213 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 214 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 215 | # html_search_language = 'en' 216 | 217 | # A dictionary with options for the search language support, empty by default. 218 | # Now only 'ja' uses this config value 219 | # html_search_options = {'type': 'default'} 220 | 221 | # The name of a javascript file (relative to the configuration directory) that 222 | # implements a search results scorer. If empty, the default will be used. 223 | # html_search_scorer = 'scorer.js' 224 | 225 | # Output file base name for HTML help builder. 226 | htmlhelp_basename = "numerapidoc" 227 | 228 | # -- Options for LaTeX output --------------------------------------------- 229 | 230 | latex_elements = { 231 | # The paper size ('letterpaper' or 'a4paper'). 232 | #'papersize': 'letterpaper', 233 | # The font size ('10pt', '11pt' or '12pt'). 234 | #'pointsize': '10pt', 235 | # Additional stuff for the LaTeX preamble. 236 | #'preamble': '', 237 | # Latex figure (float) alignment 238 | #'figure_align': 'htbp', 239 | } 240 | 241 | # Grouping the document tree into LaTeX files. List of tuples 242 | # (source start file, target name, title, 243 | # author, documentclass [howto, manual, or own class]). 244 | latex_documents = [ 245 | (master_doc, "NumerAPI.tex", u"NumerAPI Documentation", u"uuazed", "manual") 246 | ] 247 | 248 | # The name of an image file (relative to this directory) to place at the top of 249 | # the title page. 250 | # latex_logo = None 251 | 252 | # For "manual" documents, if this is true, then toplevel headings are parts, 253 | # not chapters. 254 | # latex_use_parts = False 255 | 256 | # If true, show page references after internal links. 257 | # latex_show_pagerefs = False 258 | 259 | # If true, show URL addresses after external links. 260 | # latex_show_urls = False 261 | 262 | # Documents to append as an appendix to all manuals. 263 | # latex_appendices = [] 264 | 265 | # If false, no module index is generated. 266 | # latex_domain_indices = True 267 | 268 | 269 | # -- Options for manual page output --------------------------------------- 270 | 271 | # One entry per manual page. List of tuples 272 | # (source start file, name, description, authors, manual section). 273 | man_pages = [(master_doc, "numerapi", u"NumerAPI Documentation", [author], 1)] 274 | 275 | # If true, show URL addresses after external links. 276 | # man_show_urls = False 277 | 278 | 279 | # -- Options for Texinfo output ------------------------------------------- 280 | 281 | # Grouping the document tree into Texinfo files. List of tuples 282 | # (source start file, target name, title, author, 283 | # dir menu entry, description, category) 284 | texinfo_documents = [ 285 | ( 286 | master_doc, 287 | "NumerAPI", 288 | u"NumerAPI Documentation", 289 | author, 290 | "NumerAPI", 291 | "One line description of project.", 292 | "Miscellaneous", 293 | ) 294 | ] 295 | 296 | # Documents to append as an appendix to all manuals. 297 | # texinfo_appendices = [] 298 | 299 | # If false, no module index is generated. 300 | # texinfo_domain_indices = True 301 | 302 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 303 | # texinfo_show_urls = 'footnote' 304 | 305 | # If true, do not generate a @detailmenu in the "Top" node's menu. 306 | # texinfo_no_detailmenu = False 307 | 308 | 309 | # -- Options for Epub output ---------------------------------------------- 310 | 311 | # Bibliographic Dublin Core info. 312 | epub_title = project 313 | epub_author = author 314 | epub_publisher = author 315 | epub_copyright = copyright 316 | 317 | # The basename for the epub file. It defaults to the project name. 318 | # epub_basename = project 319 | 320 | # The HTML theme for the epub output. Since the default themes are not 321 | # optimized for small screen space, using the same theme for HTML and epub 322 | # output is usually not wise. This defaults to 'epub', a theme designed to save 323 | # visual space. 324 | # epub_theme = 'epub' 325 | 326 | # The language of the text. It defaults to the language option 327 | # or 'en' if the language is not set. 328 | # epub_language = '' 329 | 330 | # The scheme of the identifier. Typical schemes are ISBN or URL. 331 | # epub_scheme = '' 332 | 333 | # The unique identifier of the text. This can be a ISBN number 334 | # or the project homepage. 335 | # epub_identifier = '' 336 | 337 | # A unique identification for the text. 338 | # epub_uid = '' 339 | 340 | # A tuple containing the cover image and cover page html template filenames. 341 | # epub_cover = () 342 | 343 | # A sequence of (type, uri, title) tuples for the guide element of content.opf. 344 | # epub_guide = () 345 | 346 | # HTML files that should be inserted before the pages created by sphinx. 347 | # The format is a list of tuples containing the path and title. 348 | # epub_pre_files = [] 349 | 350 | # HTML files that should be inserted after the pages created by sphinx. 351 | # The format is a list of tuples containing the path and title. 352 | # epub_post_files = [] 353 | 354 | # A list of files that should not be packed into the epub file. 355 | # epub_exclude_files = ["search.html"] 356 | 357 | # The depth of the table of contents in toc.ncx. 358 | # epub_tocdepth = 3 359 | 360 | # Allow duplicate toc entries. 361 | # epub_tocdup = True 362 | 363 | # Choose between 'default' and 'includehidden'. 364 | # epub_tocscope = 'default' 365 | 366 | # Fix unsupported image types using the Pillow. 367 | # epub_fix_images = False 368 | 369 | # Scale large images. 370 | # epub_max_image_width = 0 371 | 372 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 373 | # epub_show_urls = 'inline' 374 | 375 | # If false, no index is generated. 376 | # epub_use_index = True 377 | 378 | #intersphinx_mapping = { 379 | # "python": ("https://docs.python.org/3/", None), 380 | # "urllib3": ("https://urllib3.readthedocs.io/en/latest", None), 381 | #} 382 | 383 | 384 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. mdinclude:: ../README.md 2 | 3 | Contents 4 | -------- 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | changelog 10 | license 11 | 12 | Indices and tables 13 | ================== 14 | 15 | * :ref:`genindex` 16 | * :ref:`modindex` 17 | * :ref:`search` 18 | -------------------------------------------------------------------------------- /docs/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | .. literalinclude:: ../LICENSE 4 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | m2r 2 | -------------------------------------------------------------------------------- /numerapi/__init__.py: -------------------------------------------------------------------------------- 1 | """ Numerai Python API""" 2 | 3 | from importlib.metadata import version, PackageNotFoundError 4 | 5 | try: 6 | __version__ = version("package-name") 7 | except PackageNotFoundError: 8 | __version__ = 'unknown' 9 | 10 | 11 | # pylint: disable=wrong-import-position 12 | from numerapi.numerapi import NumerAPI 13 | from numerapi.signalsapi import SignalsAPI 14 | from numerapi.cryptoapi import CryptoAPI 15 | # pylint: enable=wrong-import-position 16 | -------------------------------------------------------------------------------- /numerapi/base_api.py: -------------------------------------------------------------------------------- 1 | """Parts of the API that is shared between Signals and Classic""" 2 | 3 | import os 4 | import datetime 5 | import logging 6 | from typing import Dict, List, Union, Tuple 7 | from io import BytesIO 8 | import pytz 9 | 10 | import pandas as pd 11 | import requests 12 | 13 | from numerapi import utils 14 | 15 | API_TOURNAMENT_URL = 'https://api-tournament.numer.ai' 16 | 17 | 18 | class Api: 19 | """Wrapper around the Numerai API""" 20 | 21 | def __init__(self, public_id=None, secret_key=None, verbosity="INFO", 22 | show_progress_bars=True): 23 | """ 24 | initialize Numerai API wrapper for Python 25 | 26 | Args: 27 | public_id (str): first part of your token generated at 28 | Numer.ai->Account->Custom API keys 29 | secret_key (str): second part of your token generated at 30 | Numer.ai->Account->Custom API keys 31 | verbosity (str): indicates what level of messages should be 32 | displayed. valid values are "debug", "info", "warning", 33 | "error" and "critical" 34 | show_progress_bars (bool): flag to turn of progress bars 35 | """ 36 | 37 | # set up logging 38 | self.logger = logging.getLogger(__name__) 39 | numeric_log_level = getattr(logging, verbosity.upper()) 40 | log_format = "%(asctime)s %(levelname)s %(name)s: %(message)s" 41 | logging.basicConfig(format=log_format, level=numeric_log_level) 42 | 43 | self._login(public_id, secret_key) 44 | 45 | self.show_progress_bars = show_progress_bars 46 | self.tournament_id = 0 47 | self.global_data_dir = "." 48 | 49 | def _login(self, public_id: str | None = None, secret_key: str | None = None) -> None: 50 | # check env variables if not set 51 | if not public_id or not secret_key: 52 | public_id, secret_key = utils.load_secrets() 53 | 54 | if public_id and secret_key: 55 | self.token = (public_id, secret_key) 56 | elif not public_id and not secret_key: 57 | self.token = None 58 | else: 59 | self.logger.warning( 60 | "You need to supply both a public id and a secret key.") 61 | self.token = None 62 | 63 | def _handle_call_error(self, errors): 64 | if isinstance(errors, list): 65 | for error in errors: 66 | if "message" in error: 67 | msg = error['message'] 68 | self.logger.error(msg) 69 | elif isinstance(errors, dict): 70 | if "detail" in errors: 71 | msg = errors['detail'] 72 | self.logger.error(msg) 73 | return msg 74 | 75 | def raw_query(self, query: str, variables: Dict | None = None, 76 | authorization: bool = False, 77 | *, retries: int = 3, delay: int = 5, backoff: int = 2): 78 | """Send a raw request to the Numerai's GraphQL API. 79 | 80 | This function allows to build your own queries and fetch results from 81 | Numerai's GraphQL API. Checkout 82 | https://medium.com/numerai/getting-started-with-numerais-new-tournament-api-77396e895e72 83 | for an introduction and https://api-tournament.numer.ai/ for the 84 | documentation. 85 | 86 | Args: 87 | query (str): your query 88 | variables (dict, optional): dict of variables 89 | authorization (bool, optional): does the request require 90 | authorization, defaults to `False` 91 | retries (int): for 5XX errors, how often should numerapi retry 92 | delay (int): in case of retries, how many seconds to wait between tries 93 | backoff (int): in case of retries, multiplier to increase the delay between retries 94 | 95 | Returns: 96 | dict: Result of the request 97 | 98 | Raises: 99 | ValueError: if something went wrong with the requests. For example, 100 | this could be a wrongly formatted query or a problem at 101 | Numerai's end. Have a look at the error messages, in most cases 102 | the problem is obvious. 103 | 104 | Example: 105 | >>> query = '''query($tournament: Int!) 106 | {rounds(tournament: $tournament number: 0) 107 | {number}}''' 108 | >>> args = {'tournament': 1} 109 | >>> NumerAPI().raw_query(query, args) 110 | {'data': {'rounds': [{'number': 104}]}} 111 | """ 112 | body = {'query': query, 113 | 'variables': variables} 114 | self.logger.debug(body) 115 | headers = {'Content-type': 'application/json', 116 | 'Accept': 'application/json'} 117 | if authorization: 118 | if self.token: 119 | public_id, secret_key = self.token 120 | headers['Authorization'] = f'Token {public_id}${secret_key}' 121 | else: 122 | raise ValueError("API keys required for this action.") 123 | 124 | result = utils.post_with_err_handling( 125 | API_TOURNAMENT_URL, body, headers, 126 | retries=retries, delay=delay, backoff=backoff) 127 | 128 | if result and "errors" in result: 129 | err = self._handle_call_error(result['errors']) 130 | # fail! 131 | raise ValueError(err) 132 | return result 133 | 134 | def list_datasets(self, round_num: int | None = None) -> List[str]: 135 | """List of available data files 136 | 137 | Args: 138 | round_num (int, optional): tournament round you are interested in. 139 | defaults to the current round 140 | Returns: 141 | list of str: filenames 142 | Example: 143 | >>> NumerAPI().list_datasets() 144 | [ 145 | "numerai_training_data.csv", 146 | "numerai_training_data.parquet", 147 | "numerai_validation_data.csv", 148 | "numerai_validation_data.parquet" 149 | ] 150 | """ 151 | query = """ 152 | query ($round: Int 153 | $tournament: Int) { 154 | listDatasets(round: $round 155 | tournament: $tournament) 156 | }""" 157 | args = {'round': round_num, "tournament": self.tournament_id} 158 | return self.raw_query(query, args)['data']['listDatasets'] 159 | 160 | def download_dataset(self, filename: str = None, 161 | dest_path: str | None = None, 162 | round_num: int | None = None) -> str: 163 | """ Download specified file for the given round. 164 | 165 | Args: 166 | filename (str, optional): file to be downloaded 167 | dest_path (str, optional): complete path where the file should be 168 | stored, defaults to the same name as the source file 169 | round_num (int, optional): tournament round you are interested in. 170 | defaults to the current round 171 | 172 | Returns: 173 | str: path of the downloaded file 174 | 175 | Example: 176 | >>> filenames = NumerAPI().list_datasets() 177 | >>> NumerAPI().download_dataset(filenames[0]}") 178 | """ 179 | if dest_path is None: 180 | dest_path = filename 181 | 182 | if self.global_data_dir != ".": 183 | dest_path = os.path.join(self.global_data_dir, dest_path) 184 | 185 | # if directories are used, ensure they exist 186 | dirs = os.path.dirname(dest_path) 187 | if dirs: 188 | os.makedirs(dirs, exist_ok=True) 189 | 190 | query = """ 191 | query ($filename: String! 192 | $round: Int) { 193 | dataset(filename: $filename 194 | round: $round) 195 | } 196 | """ 197 | args = {'filename': filename, "round": round_num} 198 | 199 | dataset_url = self.raw_query(query, args)['data']['dataset'] 200 | utils.download_file(dataset_url, dest_path, self.show_progress_bars) 201 | return dest_path 202 | 203 | def set_global_data_dir(self, directory: str): 204 | """Set directory used for downloading files 205 | 206 | Args: 207 | directory (str): directory to be used 208 | """ 209 | self.global_data_dir = directory 210 | # create folder if necessary 211 | os.makedirs(directory, exist_ok=True) 212 | 213 | def get_account(self) -> Dict: 214 | """Get all information about your account! 215 | 216 | Returns: 217 | dict: user information including the fields: 218 | 219 | * assignedEthAddress (`str`) 220 | * availableNmr (`decimal.Decimal`) 221 | * availableUsd (`decimal.Decimal`) 222 | * email (`str`) 223 | * id (`str`) 224 | * insertedAt (`datetime`) 225 | * mfaEnabled (`bool`) 226 | * status (`str`) 227 | * username (`str`) 228 | * apiTokens (`list`) each with the following fields: 229 | * name (`str`) 230 | * public_id (`str`) 231 | * scopes (`list of str`) 232 | * models 233 | * username 234 | * id 235 | * submissions 236 | * v2Stake 237 | * status (`str`) 238 | * txHash (`str`) 239 | 240 | Example: 241 | >>> api = NumerAPI(secret_key="..", public_id="..") 242 | >>> api.get_account() 243 | {'apiTokens': [ 244 | {'name': 'tokenname', 245 | 'public_id': 'BLABLA', 246 | 'scopes': ['upload_submission', 'stake', ..] 247 | }, ..], 248 | 'assignedEthAddress': '0x0000000000000000000000000001', 249 | 'availableNmr': Decimal('99.01'), 250 | 'email': 'username@example.com', 251 | 'id': '1234-ABC..', 252 | 'insertedAt': datetime.datetime(2018, 1, 1, 2, 16, 48), 253 | 'mfaEnabled': False, 254 | 'status': 'VERIFIED', 255 | 'username': 'cool username', 256 | } 257 | """ 258 | query = """ 259 | query { 260 | account { 261 | username 262 | walletAddress 263 | availableNmr 264 | email 265 | id 266 | mfaEnabled 267 | status 268 | insertedAt 269 | models { 270 | id 271 | name 272 | submissions { 273 | id 274 | filename 275 | } 276 | v2Stake { 277 | status 278 | txHash 279 | } 280 | } 281 | apiTokens { 282 | name 283 | public_id 284 | scopes 285 | } 286 | } 287 | } 288 | """ 289 | data = self.raw_query(query, authorization=True)['data']['account'] 290 | # convert strings to python objects 291 | utils.replace(data, "insertedAt", utils.parse_datetime_string) 292 | utils.replace(data, "availableNmr", utils.parse_float_string) 293 | return data 294 | 295 | def models_of_account(self, account) -> Dict[str, str]: 296 | """Get all models (name and id) of an account 297 | 298 | Args: 299 | account (str): account name 300 | 301 | Returns: 302 | dict: modelname->model_id mapping, string->string 303 | 304 | Example: 305 | >>> api = NumerAPI() 306 | >>> NumerAPI().models_of_account("uuazed") 307 | {'uuazed': '9b157d9b-ce61-4ab5-9413-413f13a0c0a6', ...} 308 | """ 309 | query = """ 310 | query($username: Str! 311 | $tournament: Int) { 312 | accountProfile(username: $username 313 | tournament: $tournament){ 314 | models { 315 | id 316 | displayName 317 | } 318 | } 319 | } 320 | """ 321 | args = {"username": account, "tournament": self.tournament_id} 322 | data = self.raw_query(query, args)['data']['accountProfile']['models'] 323 | return {item["displayName"]: item["id"] 324 | for item in sorted(data, key=lambda x: x["displayName"])} 325 | 326 | def get_models(self, tournament: int = None) -> Dict: 327 | """Get mapping of account model names to model ids for convenience 328 | 329 | Args: 330 | tournament (int): ID of the tournament (optional) 331 | 332 | Returns: 333 | dict: modelname->model_id mapping, string->string 334 | 335 | Example: 336 | >>> api = NumerAPI(secret_key="..", public_id="..") 337 | >>> model = api.get_models() 338 | {'uuazed': '9b157d9b-ce61-4ab5-9413-413f13a0c0a6'} 339 | """ 340 | query = """ 341 | query { 342 | account { 343 | models { 344 | id 345 | name 346 | tournament 347 | } 348 | } 349 | } 350 | """ 351 | if tournament is None: 352 | tournament = self.tournament_id 353 | data = self.raw_query( 354 | query, authorization=True)['data']['account']['models'] 355 | mapping = { 356 | model['name']: model['id'] for model in data 357 | if model['tournament'] == tournament 358 | } 359 | return mapping 360 | 361 | def get_current_round(self, tournament: int | None = None) -> int | None: 362 | """Get number of the current active round. 363 | 364 | Args: 365 | tournament (int): ID of the tournament (optional) 366 | 367 | Returns: 368 | int: number of the current active round 369 | 370 | Example: 371 | >>> NumerAPI().get_current_round() 372 | 104 373 | """ 374 | if tournament is None: 375 | tournament = self.tournament_id 376 | # zero is an alias for the current round! 377 | query = ''' 378 | query($tournament: Int!) { 379 | rounds(tournament: $tournament 380 | number: 0) { 381 | number 382 | } 383 | } 384 | ''' 385 | arguments = {'tournament': tournament} 386 | data = self.raw_query(query, arguments)['data']['rounds'][0] 387 | if data is None: 388 | return None 389 | round_num = data["number"] 390 | return round_num 391 | 392 | def set_bio(self, model_id: str, bio: str) -> bool: 393 | """Set bio field for a model id. 394 | 395 | Args: 396 | model_id (str): Target model UUID 397 | bio (str) 398 | 399 | Returns: 400 | bool: if the bio was changed successfully 401 | 402 | Example: 403 | >>> napi = numerapi.NumerAPI() 404 | >>> model_id = napi.get_models()["uuazed"] 405 | >>> napi.set_bio(model_id, "This model stinks.") 406 | True 407 | """ 408 | mutation = ''' 409 | mutation($value: String! 410 | $modelId: String) { 411 | setUserBio(value: $value 412 | modelId: $modelId) 413 | } 414 | ''' 415 | arguments = {'value': bio, 'modelId': model_id} 416 | res = self.raw_query(mutation, arguments, authorization=True) 417 | return res["data"]["setUserBio"] 418 | 419 | def set_link(self, model_id: str, link_text: str, link: str) -> bool: 420 | """Set link field for a model id. 421 | 422 | Args: 423 | model_id (str): Target model UUID 424 | link_test (str) 425 | link (str) 426 | 427 | Returns: 428 | bool: if the bio was changed successfully 429 | 430 | Example: 431 | >>> napi = numerapi.NumerAPI() 432 | >>> model_id = napi.get_models()["uuazed"] 433 | >>> napi.set_link(model_id, "buy my predictions", "numerbay.ai") 434 | True 435 | """ 436 | mutation = ''' 437 | mutation($linkUrl: String! 438 | $linkText: String 439 | $modelId: String) { 440 | setUserLink(linkText: $linkText 441 | linkUrl: $linkUrl 442 | modelId: $modelId) 443 | } 444 | ''' 445 | args = {'linkUrl': link, "linkText": link_text, 'modelId': model_id} 446 | res = self.raw_query(mutation, args, authorization=True) 447 | return res["data"]["setUserLink"] 448 | 449 | def wallet_transactions(self) -> List: 450 | """Get all transactions in your wallet. 451 | 452 | Returns: 453 | list: List of dicts with the following structure: 454 | 455 | * from (`str`) 456 | * posted (`bool`) 457 | * status (`str`) 458 | * to (`str`) 459 | * txHash (`str`) 460 | * amount (`decimal.Decimal`) 461 | * time (`datetime`) 462 | * tournament (`int`) 463 | 464 | Example: 465 | >>> api = NumerAPI(secret_key="..", public_id="..") 466 | >>> api.wallet_transactions() 467 | [{'amount': Decimal('1.000000000000000000'), 468 | 'from': '0x000000000000000000000000000000000000313bc', 469 | 'status': 'confirmed', 470 | 'time': datetime.datetime(2023, 4, 19, 13, 28, 45), 471 | 'to': '0x000000000000000000000000000000000006621', 472 | 'tournament': None, 473 | 'txHash': '0xeasdfkjaskljf314451234', 474 | 'type': 'withdrawal'}, 475 | ... 476 | ] 477 | """ 478 | query = """ 479 | query { 480 | account { 481 | walletTxns { 482 | amount 483 | from 484 | status 485 | to 486 | time 487 | tournament 488 | txHash 489 | type 490 | } 491 | } 492 | } 493 | """ 494 | txs = self.raw_query( 495 | query, authorization=True)['data']['account']['walletTxns'] 496 | # convert strings to python objects 497 | for transaction in txs: 498 | utils.replace(transaction, "time", utils.parse_datetime_string) 499 | utils.replace(transaction, "amount", utils.parse_float_string) 500 | return txs 501 | 502 | def set_submission_webhook(self, model_id: str, 503 | webhook: str = None) -> bool: 504 | """Set a model's submission webhook used in Numerai Compute. 505 | Read More: https://docs.numer.ai/tournament/compute 506 | 507 | Args: 508 | model_id (str): Target model UUID 509 | 510 | webhook (str): The compute webhook to trigger this model 511 | 512 | Returns: 513 | bool: confirmation that your webhook has been set 514 | 515 | Example: 516 | >>> api = NumerAPI(secret_key="..", public_id="..") 517 | >>> api.set_submission_webhook(model_id="..", webhook="..") 518 | True 519 | """ 520 | query = ''' 521 | mutation ( 522 | $modelId: String! 523 | $newSubmissionWebhook: String 524 | ) { 525 | setSubmissionWebhook( 526 | modelId: $modelId 527 | newSubmissionWebhook: $newSubmissionWebhook 528 | ) 529 | } 530 | ''' 531 | arguments = {'modelId': model_id, 'newSubmissionWebhook': webhook} 532 | res = self.raw_query(query, arguments, authorization=True) 533 | return res['data']['setSubmissionWebhook'] == "true" 534 | 535 | def _upload_auth(self, endpoint: str, file_path: str, tournament: int, 536 | model_id: str) -> Dict[str, str]: 537 | auth_query = f''' 538 | query($filename: String! 539 | $tournament: Int! 540 | $modelId: String) {{ 541 | {endpoint}(filename: $filename 542 | tournament: $tournament 543 | modelId: $modelId) {{ 544 | filename 545 | url 546 | }} 547 | }} 548 | ''' 549 | arguments = {'filename': os.path.basename(file_path), 550 | 'tournament': tournament, 551 | 'modelId': model_id} 552 | return self.raw_query( 553 | auth_query, arguments, 554 | authorization=True)['data'][endpoint] 555 | 556 | def upload_diagnostics(self, file_path: str = "predictions.csv", 557 | tournament: int | None = None, 558 | model_id: str | None = None, 559 | df: pd.DataFrame | None = None) -> str: 560 | """Upload predictions to diagnostics from file. 561 | 562 | Args: 563 | file_path (str): CSV file with predictions that will get uploaded 564 | tournament (int): ID of the tournament (optional, defaults to None) 565 | model_id (str): Target model UUID (required for accounts with 566 | multiple models) 567 | df (pandas.DataFrame): pandas DataFrame to upload, if function is 568 | given df and file_path, df will be uploaded. 569 | 570 | Returns: 571 | str: diagnostics_id 572 | 573 | Example: 574 | >>> api = NumerAPI(secret_key="..", public_id="..") 575 | >>> model_id = api.get_models()['uuazed'] 576 | >>> api.upload_diagnostics("prediction.cvs", model_id=model_id) 577 | '93c46857-fed9-4594-981e-82db2b358daf' 578 | >>> # upload from pandas DataFrame directly: 579 | >>> api.upload_diagnostics(df=predictions_df, model_id=model_id) 580 | """ 581 | self.logger.info("uploading diagnostics...") 582 | 583 | # write the pandas DataFrame as a binary buffer if provided 584 | buffer_csv = None 585 | if tournament is None: 586 | tournament = self.tournament_id 587 | 588 | if df is not None: 589 | buffer_csv = BytesIO(df.to_csv(index=False).encode()) 590 | buffer_csv.name = file_path 591 | 592 | upload_auth = self._upload_auth( 593 | 'diagnosticsUploadAuth', file_path, tournament, model_id) 594 | 595 | with open(file_path, 'rb') if df is None else buffer_csv as file: 596 | requests.put(upload_auth['url'], data=file.read(), timeout=600) 597 | create_query = ''' 598 | mutation($filename: String! 599 | $tournament: Int! 600 | $modelId: String) { 601 | createDiagnostics(filename: $filename 602 | tournament: $tournament 603 | modelId: $modelId) { 604 | id 605 | } 606 | }''' 607 | arguments = {'filename': upload_auth['filename'], 608 | 'tournament': tournament, 609 | 'modelId': model_id} 610 | create = self.raw_query(create_query, arguments, authorization=True) 611 | diagnostics_id = create['data']['createDiagnostics']['id'] 612 | return diagnostics_id 613 | 614 | def diagnostics(self, model_id: str, diagnostics_id: str | None = None) -> Dict: 615 | """Fetch results of diagnostics run 616 | 617 | Args: 618 | model_id (str): Target model UUID (required for accounts with 619 | multiple models) 620 | diagnostics_id (str, optional): id returned by "upload_diagnostics" 621 | 622 | Returns: 623 | dict: diagnostic results with the following content: 624 | 625 | * validationCorrMean (`float`) 626 | * validationCorrSharpe (`float`) 627 | * examplePredsCorrMean (`float`) 628 | * validationMmcStd (`float`) 629 | * validationMmcSharpe (`float`) 630 | * validationCorrPlusMmcSharpeDiff (`float`) 631 | * validationMmcStdRating (`float`) 632 | * validationMmcMeanRating (`float`) 633 | * validationCorrPlusMmcSharpeDiffRating (`float`) 634 | * perEraDiagnostics (`list`) each with the following fields: 635 | * era (`int`) 636 | * examplePredsCorr (`float`) 637 | * validationCorr (`float`) 638 | * validationCorrV4 (`float`) 639 | * validationFeatureCorrMax (`float`) 640 | * validationFeatureNeutralCorr (`float`) 641 | * validationFeatureNeutralCorrV3 642 | * validationMmc (`float`) 643 | * validationFncV4 (`float`) 644 | * validationIcV2 (`float`) 645 | * validationRic (`float`) 646 | * validationBmc (`float`) 647 | * validationCorrPlusMmcStd (`float`) 648 | * validationMmcMean (`float`) 649 | * validationCorrStdRating (`float`) 650 | * validationCorrPlusMmcSharpe (`float`) 651 | * validationMaxDrawdownRating (`float`) 652 | * validationFeatureNeutralCorrMean (`float`) 653 | * validationCorrPlusMmcMean (`float`) 654 | * validationFeatureCorrMax (`float`) 655 | * status (`string`), 656 | * validationCorrMeanRating (`float`) 657 | * validationFeatureNeutralCorrMeanRating (`float`) 658 | * validationCorrSharpeRating (`float`) 659 | * validationCorrPlusMmcMeanRating (`float`) 660 | * message (`string`) 661 | * validationMmcSharpeRating (`float`) 662 | * updatedAt (`datetime`) 663 | * validationFeatureCorrMaxRating (`float`) 664 | * validationCorrPlusMmcSharpeRating (`float`) 665 | * trainedOnVal (`bool`) 666 | * validationCorrStd (`float`) 667 | * erasAcceptedCount (`int`) 668 | * validationMaxDrawdown (`float`) 669 | * validationCorrPlusMmcStdRating (`float`) 670 | * validationAdjustedSharpe (`float`) 671 | * validationApy (`float`) 672 | * validationAutocorr (`float`) 673 | * validationCorrCorrWExamplePreds (`float`) 674 | * validationCorrMaxDrawdown (`float`) 675 | * validationCorrV4CorrWExamplePreds (`float`) 676 | * validationCorrV4MaxDrawdown (`float`) 677 | * validationCorrV4Mean (`float`) 678 | * validationBmcMean (`float`) 679 | * validationCorrV4Sharpe (`float`) 680 | * validationCorrV4Std (`float`) 681 | * validationFeatureNeutralCorrV3Mean (`float`) 682 | * validationFeatureNeutralCorrV3MeanRating (`float`) 683 | * validationFncV4CorrWExamplePreds (`float`) 684 | * validationFncV4MaxDrawdown (`float`) 685 | * validationFncV4Mean (`float`) 686 | * validationFncV4Sharpe (`float`) 687 | * validationFncV4Std (`float`) 688 | * validationIcV2CorrWExamplePreds (`float`) 689 | * validationIcV2MaxDrawdown (`float`) 690 | * validationIcV2Mean (`float`) 691 | * validationIcV2Sharpe (`float`) 692 | * validationIcV2Std (`float`) 693 | * validationRicCorrWExamplePreds (`float`) 694 | * validationRicMaxDrawdown (`float`) 695 | * validationRicMean (`float`) 696 | * validationRicSharpe (`float`) 697 | * validationRicStd (`float`) 698 | 699 | Example: 700 | >>> napi = NumerAPI(secret_key="..", public_id="..") 701 | >>> model_id = napi.get_models()['uuazed'] 702 | >>> api.upload_diagnostics("prediction.cvs", model_id=model_id) 703 | '93c46857-fed9-4594-981e-82db2b358daf' 704 | >>> napi.diagnostic(model_id) 705 | {"validationCorrMean": 0.53231, 706 | ... 707 | } 708 | 709 | """ 710 | query = ''' 711 | query($id: String 712 | $modelId: String!) { 713 | diagnostics(id: $id 714 | modelId: $modelId) { 715 | erasAcceptedCount 716 | examplePredsCorrMean 717 | message 718 | perEraDiagnostics { 719 | era 720 | examplePredsCorr 721 | validationCorr 722 | validationCorrV4 723 | validationFeatureCorrMax 724 | validationFeatureNeutralCorr 725 | validationFeatureNeutralCorrV3 726 | validationMmc 727 | validationFncV4 728 | validationIcV2 729 | validationRic 730 | validationBmc 731 | } 732 | status 733 | trainedOnVal 734 | updatedAt 735 | validationCorrMean 736 | validationCorrMeanRating 737 | validationCorrPlusMmcMean 738 | validationCorrPlusMmcMeanRating 739 | validationCorrPlusMmcSharpe 740 | validationCorrPlusMmcSharpeDiff 741 | validationCorrPlusMmcSharpeDiffRating 742 | validationCorrPlusMmcSharpeRating 743 | validationCorrPlusMmcStd 744 | validationCorrPlusMmcStdRating 745 | validationCorrSharpe 746 | validationCorrSharpeRating 747 | validationCorrStd 748 | validationCorrStdRating 749 | validationFeatureCorrMax 750 | validationFeatureCorrMaxRating 751 | validationFeatureNeutralCorrMean 752 | validationFeatureNeutralCorrMeanRating 753 | validationMaxDrawdown 754 | validationMaxDrawdownRating 755 | validationMmcMean 756 | validationMmcMeanRating 757 | validationMmcSharpe 758 | validationMmcSharpeRating 759 | validationMmcStd 760 | validationMmcStdRating 761 | validationBmcMean 762 | 763 | validationAdjustedSharpe 764 | validationApy 765 | validationAutocorr 766 | validationCorrCorrWExamplePreds 767 | validationCorrMaxDrawdown 768 | validationCorrV4CorrWExamplePreds 769 | validationCorrV4MaxDrawdown 770 | validationCorrV4Mean 771 | validationCorrV4Sharpe 772 | validationCorrV4Std 773 | validationFeatureNeutralCorrV3Mean 774 | validationFeatureNeutralCorrV3MeanRating 775 | validationFncV4CorrWExamplePreds 776 | validationFncV4MaxDrawdown 777 | validationFncV4Mean 778 | validationFncV4Sharpe 779 | validationFncV4Std 780 | validationIcV2CorrWExamplePreds 781 | validationIcV2MaxDrawdown 782 | validationIcV2Mean 783 | validationIcV2Sharpe 784 | validationIcV2Std 785 | validationRicCorrWExamplePreds 786 | validationRicMaxDrawdown 787 | validationRicMean 788 | validationRicSharpe 789 | validationRicStd 790 | } 791 | } 792 | ''' 793 | args = {'modelId': model_id, 'id': diagnostics_id} 794 | results = self.raw_query( 795 | query, args, authorization=True)['data']['diagnostics'] 796 | utils.replace(results, "updatedAt", utils.parse_datetime_string) 797 | return results 798 | 799 | def round_model_performances_v2(self, model_id: str): 800 | """Fetch round model performance of a user. 801 | 802 | Args: 803 | model_id (str) 804 | 805 | Returns: 806 | list of dicts: list of round model performance entries 807 | 808 | For each entry in the list, there is a dict with the following 809 | content: 810 | 811 | * atRisk (`float`) 812 | * corrMultiplier (`float` or None) 813 | * tcMultiplier (`float` or None) 814 | * roundNumber (`int`) 815 | * roundOpenTime (`datetime`) 816 | * roundResolveTime (`datetime`) 817 | * roundResolved (`bool`) 818 | * roundTarget (`str`) 819 | * submissionScores (`dict`) 820 | * date (`datetime`) 821 | * day (`int`) 822 | * displayName (`str`): name of the metric 823 | * payoutPending (`float`) 824 | * payoutSettled (`float`) 825 | * percentile (`float`) 826 | * value (`float`): value of the metric 827 | """ 828 | 829 | query = """ 830 | query($modelId: String! 831 | $tournament: Int!) { 832 | v2RoundModelPerformances(modelId: $modelId 833 | tournament: $tournament) { 834 | atRisk 835 | corrMultiplier 836 | tcMultiplier 837 | roundNumber, 838 | roundOpenTime, 839 | roundResolveTime, 840 | roundResolved, 841 | roundTarget, 842 | submissionScores { 843 | date, 844 | day, 845 | displayName, 846 | payoutPending, 847 | payoutSettled, 848 | percentile, 849 | value 850 | } 851 | } 852 | } 853 | """ 854 | arguments = {'modelId': model_id, 'tournament': self.tournament_id} 855 | data = self.raw_query(query, arguments)['data'] 856 | performances = data['v2RoundModelPerformances'] 857 | for perf in performances: 858 | utils.replace(perf, "roundOpenTime", utils.parse_datetime_string) 859 | utils.replace(perf, "roundResolveTime", utils.parse_datetime_string) 860 | utils.replace(perf, "atRisk", utils.parse_float_string) 861 | if perf["submissionScores"]: 862 | for submission in perf["submissionScores"]: 863 | utils.replace( 864 | submission, "date", utils.parse_datetime_string) 865 | utils.replace( 866 | submission, "payoutPending", utils.parse_float_string) 867 | utils.replace( 868 | submission, "payoutSettled",utils.parse_float_string) 869 | return performances 870 | 871 | def intra_round_scores(self, model_id: str): 872 | """Fetch intra-round scores for your model. 873 | 874 | While only the final scores are relevant for payouts, it might be 875 | interesting to look how your scores evolve throughout a round. 876 | 877 | Args: 878 | model_id (str) 879 | 880 | Returns: 881 | list of dicts: list of intra-round model performance entries 882 | 883 | For each entry in the list, there is a dict with the following 884 | content: 885 | 886 | * roundNumber (`int`) 887 | * intraRoundSubmissionScores (`dict`) 888 | * date (`datetime`) 889 | * day (`int`) 890 | * displayName (`str`): name of the metric 891 | * payoutPending (`float`) 892 | * payoutSettled (`float`) 893 | * percentile (`float`) 894 | * value (`float`): value of the metric 895 | """ 896 | 897 | query = """ 898 | query($modelId: String! 899 | $tournament: Int!) { 900 | v2RoundModelPerformances(modelId: $modelId 901 | tournament: $tournament) { 902 | roundNumber, 903 | intraRoundSubmissionScores { 904 | date, 905 | day, 906 | displayName, 907 | payoutPending, 908 | payoutSettled, 909 | percentile, 910 | value 911 | } 912 | } 913 | } 914 | """ 915 | arguments = {'modelId': model_id, 'tournament': self.tournament_id} 916 | data = self.raw_query(query, arguments)['data'] 917 | performances = data['v2RoundModelPerformances'] 918 | for perf in performances: 919 | if perf["intraRoundSubmissionScores"]: 920 | for score in perf["intraRoundSubmissionScores"]: 921 | utils.replace(score, "date", utils.parse_datetime_string) 922 | fun = utils.parse_float_string 923 | utils.replace(score, "payoutPending", fun) 924 | utils.replace(score, "payoutSettled", fun) 925 | return performances 926 | 927 | def round_model_performances(self, username: str) -> List[Dict]: 928 | """Fetch round model performance of a user. 929 | 930 | DEPRECATED - please use `round_model_performances_v2` instead 931 | """ 932 | self.logger.warning( 933 | "Deprecated. Checkout round_model_performances_v2.") 934 | return self.round_model_performances_v2(username) 935 | 936 | 937 | def stake_change(self, nmr, action: str = "decrease", 938 | model_id: str | None = None) -> Dict: 939 | """Change stake by `value` NMR. 940 | 941 | Args: 942 | nmr (float or str): amount of NMR you want to increase/decrease 943 | action (str): `increase` or `decrease` 944 | model_id (str): Target model UUID (required for accounts with 945 | multiple models) 946 | 947 | Returns: 948 | dict: stake information with the following content: 949 | 950 | * dueDate (`datetime`) 951 | * status (`str`) 952 | * requestedAmount (`decimal.Decimal`) 953 | * type (`str`) 954 | 955 | Example: 956 | >>> api = NumerAPI(secret_key="..", public_id="..") 957 | >>> model = api.get_models()['uuazed'] 958 | >>> api.stake_change(10, "decrease", model) 959 | {'dueDate': None, 960 | 'requestedAmount': decimal.Decimal('10'), 961 | 'type': 'decrease', 962 | 'status': ''} 963 | """ 964 | query = ''' 965 | mutation($value: String! 966 | $type: String! 967 | $tournamentNumber: Int! 968 | $modelId: String) { 969 | v2ChangeStake(value: $value 970 | type: $type 971 | modelId: $modelId 972 | tournamentNumber: $tournamentNumber) { 973 | dueDate 974 | requestedAmount 975 | status 976 | type 977 | } 978 | } 979 | ''' 980 | arguments = {'value': str(nmr), 981 | 'type': action, 982 | 'modelId': model_id, 983 | 'tournamentNumber': self.tournament_id} 984 | result = self.raw_query(query, arguments, authorization=True) 985 | stake = result['data']['v2ChangeStake'] 986 | utils.replace(stake, "requestedAmount", utils.parse_float_string) 987 | utils.replace(stake, "dueDate", utils.parse_datetime_string) 988 | return stake 989 | 990 | def stake_drain(self, model_id: str = None) -> Dict: 991 | """Completely remove your stake. 992 | 993 | Args: 994 | model_id (str): Target model UUID 995 | 996 | Returns: 997 | dict: stake information with the following content: 998 | 999 | * dueDate (`datetime`) 1000 | * status (`str`) 1001 | * requestedAmount (`decimal.Decimal`) 1002 | * type (`str`) 1003 | * drain (`bool`) 1004 | 1005 | Example: 1006 | >>> api = NumerAPI(secret_key="..", public_id="..") 1007 | >>> model_id = api.get_models()['uuazed'] 1008 | >>> api.stake_drain(model_id) 1009 | {'dueDate': None, 1010 | 'requestedAmount': decimal.Decimal('11000000'), 1011 | 'type': 'decrease', 1012 | 'status': '', 1013 | 'drain": True} 1014 | """ 1015 | query = ''' 1016 | mutation($drain: bool! 1017 | $amount: String 1018 | $modelId: String) { 1019 | releaseStake(drain: $drain 1020 | modelId: $modelId 1021 | amount: $amount) { 1022 | id 1023 | dueDate 1024 | status 1025 | type 1026 | requestedAmount 1027 | drain 1028 | } 1029 | }''' 1030 | arguments = {'drain': True, "modelId": model_id, "amount": '11000000'} 1031 | raw = self.raw_query(query, arguments, authorization=True) 1032 | return raw['data']['releaseStake'] 1033 | 1034 | def stake_decrease(self, nmr, model_id: str | None = None) -> Dict: 1035 | """Decrease your stake by `value` NMR. 1036 | 1037 | Args: 1038 | nmr (float or str): amount of NMR you want to reduce 1039 | model_id (str): Target model UUID (required for accounts with 1040 | multiple models) 1041 | tournament (int): ID of the tournament (optional, defaults to 8) 1042 | 1043 | Returns: 1044 | dict: stake information with the following content: 1045 | 1046 | * dueDate (`datetime`) 1047 | * status (`str`) 1048 | * requestedAmount (`decimal.Decimal`) 1049 | * type (`str`) 1050 | 1051 | Example: 1052 | >>> api = NumerAPI(secret_key="..", public_id="..") 1053 | >>> model = api.get_models()['uuazed'] 1054 | >>> api.stake_decrease(10, model) 1055 | {'dueDate': None, 1056 | 'requestedAmount': decimal.Decimal('10'), 1057 | 'type': 'decrease', 1058 | 'status': ''} 1059 | """ 1060 | return self.stake_change(nmr, 'decrease', model_id) 1061 | 1062 | def stake_increase(self, nmr, model_id: str | None = None) -> Dict: 1063 | """Increase your stake by `value` NMR. 1064 | 1065 | Args: 1066 | nmr (float or str): amount of additional NMR you want to stake 1067 | model_id (str): Target model UUID (required for accounts with 1068 | multiple models) 1069 | tournament (int): ID of the tournament (optional, defaults to 8) 1070 | 1071 | Returns: 1072 | dict: stake information with the following content: 1073 | 1074 | * dueDate (`datetime`) 1075 | * status (`str`) 1076 | * requestedAmount (`decimal.Decimal`) 1077 | * type (`str`) 1078 | 1079 | Example: 1080 | >>> api = NumerAPI(secret_key="..", public_id="..") 1081 | >>> model = api.get_models()['uuazed'] 1082 | >>> api.stake_increase(10, model) 1083 | {'dueDate': None, 1084 | 'requestedAmount': decimal.Decimal('10'), 1085 | 'type': 'increase', 1086 | 'status': ''} 1087 | """ 1088 | return self.stake_change(nmr, 'increase', model_id) 1089 | 1090 | def check_round_open(self) -> bool: 1091 | """Check if a round is currently open. 1092 | 1093 | Returns: 1094 | bool: True if a round is currently open for submissions, False otherwise. 1095 | 1096 | Example: 1097 | >>> NumerAPI().check_round_open() 1098 | False 1099 | """ 1100 | query = ''' 1101 | query($tournament: Int!) { 1102 | rounds(tournament: $tournament 1103 | number: 0) { 1104 | number 1105 | openTime 1106 | closeStakingTime 1107 | } 1108 | } 1109 | ''' 1110 | arguments = {'tournament': self.tournament_id} 1111 | # in some period in between rounds, "number: 0" returns Value error - 1112 | # "Current round not open for submissions", because there is no active 1113 | # round. This is caught by the try / except. 1114 | try: 1115 | raw = self.raw_query(query, arguments)['data']['rounds'][0] 1116 | except ValueError: 1117 | return False 1118 | if raw is None: 1119 | return False 1120 | open_time = utils.parse_datetime_string(raw['openTime']) 1121 | deadline = utils.parse_datetime_string(raw["closeStakingTime"]) 1122 | now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) 1123 | is_open = open_time < now < deadline 1124 | return is_open 1125 | 1126 | def check_new_round(self, hours: int = 12) -> bool: 1127 | """Check if a new round has started within the last `hours`. 1128 | 1129 | Args: 1130 | hours (int, optional): timeframe to consider, defaults to 12 1131 | 1132 | Returns: 1133 | bool: True if a new round has started, False otherwise. 1134 | 1135 | Example: 1136 | >>> NumerAPI().check_new_round() 1137 | False 1138 | """ 1139 | query = ''' 1140 | query($tournament: Int!) { 1141 | rounds(tournament: $tournament 1142 | number: 0) { 1143 | number 1144 | openTime 1145 | } 1146 | } 1147 | ''' 1148 | arguments = {'tournament': self.tournament_id} 1149 | # in some period in between rounds, "number: 0" returns Value error - 1150 | # "Current round not open for submissions", because there is no active 1151 | # round. This is caught by the try / except. 1152 | try: 1153 | raw = self.raw_query(query, arguments)['data']['rounds'][0] 1154 | except ValueError: 1155 | return False 1156 | if raw is None: 1157 | return False 1158 | open_time = utils.parse_datetime_string(raw['openTime']) 1159 | now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) 1160 | is_new_round = open_time > now - datetime.timedelta(hours=hours) 1161 | return is_new_round 1162 | 1163 | def get_account_leaderboard( 1164 | self, limit: int = 50, offset: int = 0) -> List[Dict]: 1165 | """Get the current account leaderboard 1166 | 1167 | Args: 1168 | limit (int): number of items to return (optional, defaults to 50) 1169 | offset (int): number of items to skip (optional, defaults to 0) 1170 | 1171 | Returns: 1172 | list of dicts: list of leaderboard entries 1173 | 1174 | Each dict contains the following items: 1175 | 1176 | * username (`str`) 1177 | * displayName (`str`) 1178 | * rank (`int`) 1179 | * nmrStaked (`decimal.Decimal`) 1180 | * v2Corr20 (`float`) 1181 | * cort20 (`float`) 1182 | * corrV4 (`float`) 1183 | * fncV4 (`float`) 1184 | * icV2 (`float`) 1185 | * mmc (`float`) 1186 | * ric (`float`) 1187 | * return1y (`float`) 1188 | * return3m (`float`) 1189 | * returnAllTime (`float`) 1190 | * return1yNmr (`decimal.Decimal`) 1191 | * return3mNmr (`decimal.Decimal`) 1192 | * returnAllTimeNmr (`decimal.Decimal`) 1193 | 1194 | Example: 1195 | >>> numerapi.NumerAPI().get_account_leaderboard() 1196 | [{'username': 'leonidas', 1197 | 'rank': 1, 1198 | 'nmrStaked': Decimal('3034.00'), 1199 | ... 1200 | }] 1201 | """ 1202 | query = ''' 1203 | query($limit: Int! 1204 | $offset: Int! 1205 | $tournament: Int) { 1206 | accountLeaderboard(limit: $limit 1207 | offset: $offset 1208 | tournament: $tournament) { 1209 | displayName 1210 | nmrStaked 1211 | rank 1212 | username 1213 | v2Corr20 1214 | cort20 1215 | corJ60 1216 | corrV4 1217 | fncV4 1218 | icV2 1219 | mmc 1220 | ric 1221 | return1y 1222 | return3m 1223 | returnAllTime 1224 | return1yNmr 1225 | return3mNmr 1226 | returnAllTimeNmr 1227 | } 1228 | } 1229 | ''' 1230 | args = {'limit': limit, 'offset': offset, 1231 | "tournament": self.tournament_id} 1232 | data = self.raw_query(query, args)['data']['accountLeaderboard'] 1233 | for item in data: 1234 | utils.replace(item, "nmrStaked", utils.parse_float_string) 1235 | utils.replace(item, "return1yNmr", utils.parse_float_string) 1236 | utils.replace(item, "return3mNmr", utils.parse_float_string) 1237 | utils.replace(item, "returnAllTimeNmr", utils.parse_float_string) 1238 | return data 1239 | 1240 | def modelid_to_modelname(self, model_id: str) -> str: 1241 | """Get model name from a model_id. 1242 | 1243 | Args: 1244 | model_id (str) 1245 | 1246 | Returns: 1247 | str: modelname 1248 | """ 1249 | query = """ 1250 | query($modelid: String!) { 1251 | model(modelId: $modelid) { 1252 | name 1253 | } 1254 | } 1255 | """ 1256 | arguments = {'modelid': model_id} 1257 | res = self.raw_query(query, arguments, authorization=True) 1258 | return res['data']['model']["name"] 1259 | 1260 | def pipeline_status(self, date: str = None) -> Dict: 1261 | """Get status of Numerai's scoring pipeline 1262 | 1263 | Args: 1264 | date (str, optional): date in YYYY-MM-DD format. Defaults to today. 1265 | 1266 | Returns: 1267 | dict: pipeline status information including the following fields: 1268 | * dataReadyAt (`str`) 1269 | * isScoringDay (`bool`) 1270 | * resolvedAt (`datetime`) 1271 | * scoredAt (`datetime`) 1272 | * startedAt (`datetime`) 1273 | * tournament (`str`) 1274 | 1275 | Example: 1276 | >>> napi = NumerAPI() 1277 | >>> napi.pipeline_status() 1278 | """ 1279 | if date is None: 1280 | date = datetime.date.today().isoformat() 1281 | tournament = "classic" if self.tournament_id == 8 else "signals" 1282 | query = """ 1283 | query($tournament: String! $date: String) { 1284 | pipelineStatus(date: $date, tournament: $tournament) { 1285 | dataReadyAt 1286 | isScoringDay 1287 | resolvedAt 1288 | scoredAt 1289 | startedAt 1290 | tournament 1291 | } 1292 | } 1293 | """ 1294 | arguments = {'tournament': tournament, "date": date} 1295 | res = self.raw_query(query, arguments)["data"]["pipelineStatus"] 1296 | for field in res.keys(): 1297 | if field.endswith("At"): 1298 | utils.replace(res, field, utils.parse_datetime_string) 1299 | return res 1300 | 1301 | def model_upload(self, file_path: str, 1302 | tournament: int | None = None, 1303 | model_id: str | None = None, 1304 | data_version: str | None = None, 1305 | docker_image: str | None = None) -> str: 1306 | """Upload pickled model to numerai. 1307 | 1308 | Args: 1309 | file_path (str): pickle file, needs to endwith .pkl 1310 | tournament (int): ID of the tournament (optional) 1311 | model_id (str): Target model UUID 1312 | data_version (str, optional): which data version to use. ID or name. 1313 | Check available options with 'model_upload_data_versions' 1314 | docker_image (str, optional): which docker image to use. ID or name. 1315 | Check available options with 'model_upload_docker_images' 1316 | 1317 | Returns: 1318 | str: model_upload_id 1319 | 1320 | Example: 1321 | >>> api = NumerAPI(secret_key="..", public_id="..") 1322 | >>> model_id = api.get_models()['uuazed'] 1323 | >>> api.model_upload("example.pkl", model_id=model_id) 1324 | '93c46857-fed9-4594-981e-82db2b358daf' 1325 | """ 1326 | if data_version is not None: 1327 | if not utils.is_valid_uuid(data_version): 1328 | data_versions = self.model_upload_data_versions() 1329 | if data_version not in data_versions: 1330 | msg = "'data_version' needs to be one of" 1331 | msg += f"{list(data_versions.keys())}" 1332 | raise ValueError(msg) 1333 | data_version = data_versions[data_version] 1334 | if docker_image is not None: 1335 | if not utils.is_valid_uuid(docker_image): 1336 | docker_images = self.model_upload_docker_images() 1337 | if docker_image not in docker_images: 1338 | msg = "'docker_image' needs to be one of" 1339 | msg += f"{list(docker_images.keys())}" 1340 | raise ValueError(msg) 1341 | docker_image = docker_images[docker_image] 1342 | 1343 | auth_query = ''' 1344 | query($filename: String! $modelId: String) { 1345 | computePickleUploadAuth(filename: $filename 1346 | modelId: $modelId) { 1347 | filename 1348 | url 1349 | } 1350 | } 1351 | ''' 1352 | arguments = {'filename': os.path.basename(file_path), 1353 | 'modelId': model_id} 1354 | upload_auth = self.raw_query( 1355 | auth_query, arguments, 1356 | authorization=True)['data']["computePickleUploadAuth"] 1357 | 1358 | with open(file_path, 'rb') as file: 1359 | requests.put(upload_auth['url'], data=file.read(), timeout=600) 1360 | create_query = ''' 1361 | mutation($filename: String! 1362 | $tournament: Int! 1363 | $modelId: String 1364 | $dataVersionId: String 1365 | $dockerImageId: String) { 1366 | createComputePickleUpload(filename: $filename 1367 | tournament: $tournament 1368 | modelId: $modelId 1369 | dataVersionId: $dataVersionId 1370 | dockerImageId: $dockerImageId) { 1371 | id 1372 | } 1373 | }''' 1374 | tournament = self.tournament_id if tournament is None else tournament 1375 | arguments = {'filename': upload_auth['filename'], 1376 | 'tournament': tournament, 1377 | 'modelId': model_id, 1378 | 'dataVersionId': data_version, 1379 | 'dockerImageId': docker_image} 1380 | create = self.raw_query(create_query, arguments, authorization=True) 1381 | return create['data']['createComputePickleUpload']['id'] 1382 | 1383 | def model_upload_data_versions(self) -> Dict: 1384 | """ Get available data version for model uploads 1385 | 1386 | Returns: 1387 | dict[str, str]: name to ID mapping 1388 | 1389 | Example: 1390 | >>> api = NumerAPI(secret_key="..", public_id="..") 1391 | >>> api.model_upload_data_versions() 1392 | {'v4.1': 'a76bafa1-b25a-4f22-9add-65b528a0f3d0'} 1393 | 1394 | """ 1395 | query = ''' 1396 | query { 1397 | computePickleDataVersions { 1398 | name 1399 | id 1400 | } 1401 | } 1402 | ''' 1403 | data = self.raw_query(query, authorization=True)['data'] 1404 | res = { 1405 | item["name"]: item["id"] 1406 | for item in data["computePickleDataVersions"]} 1407 | return res 1408 | 1409 | def model_upload_docker_images(self) -> Dict: 1410 | """ Get available docker images for model uploads 1411 | 1412 | Returns: 1413 | dict[str, str]: name to ID mapping 1414 | 1415 | Example: 1416 | >>> api = NumerAPI(secret_key="..", public_id="..") 1417 | >>> api.model_upload_docker_images() 1418 | {'Python 3.10': 'c72ae05e-2831-4c50-b20f-c2fe01c206ef', 1419 | 'Python 3.9': '5a32b827-cd9a-40a9-a99d-e58401120a0b', 1420 | ... 1421 | } 1422 | """ 1423 | query = ''' 1424 | query { 1425 | computePickleDockerImages { 1426 | name 1427 | id 1428 | } 1429 | } 1430 | ''' 1431 | data = self.raw_query(query, authorization=True)['data'] 1432 | res = { 1433 | item["name"]: item["id"] 1434 | for item in data["computePickleDockerImages"]} 1435 | return res 1436 | 1437 | def submission_ids(self, model_id: str): 1438 | """ Get all submission ids from a model 1439 | 1440 | Args: 1441 | model_id (str) 1442 | 1443 | Returns: 1444 | list of dicts: list of submissions 1445 | 1446 | For each entry in the list, there is a dict with the following 1447 | content: 1448 | 1449 | * insertedAt (`datetime`) 1450 | * filename (`str`) 1451 | * id (`str`) 1452 | 1453 | Example: 1454 | >>> api = NumerAPI(secret_key="..", public_id="..") 1455 | >>> model_id = napi.get_models()["uuazed"] 1456 | >>> api.submission_ids(model_id) 1457 | """ 1458 | query = """ 1459 | query($modelId: String) { 1460 | submissions(modelId: $modelId) { 1461 | id 1462 | filename 1463 | insertedAt 1464 | } 1465 | } 1466 | """ 1467 | raw = self.raw_query(query, {"modelId": model_id}, authorization=True) 1468 | data = raw["data"]["submissions"] 1469 | utils.replace(data, "insertedAt", utils.parse_datetime_string) 1470 | return data 1471 | 1472 | def download_submission(self, submission_id: str | None = None, 1473 | model_id: str | None = None, 1474 | dest_path: str | None = None) -> str: 1475 | """ Download previous submissions from numerai 1476 | 1477 | Args: 1478 | submission_id (str, optional): the submission to be downloaded 1479 | model_id (str, optional): if provided, the latest submission of that 1480 | model gets downloaded 1481 | dest_path (str, optional): where to save the downloaded file 1482 | 1483 | Returns: 1484 | str: path to downloaded file 1485 | 1486 | Example: 1487 | >>> # fetch latest submission 1488 | >>> api = NumerAPI(secret_key="..", public_id="..") 1489 | >>> model_id = api.get_models()["uuazed"] 1490 | >>> api.download_submission(model_id=model_id) 1491 | >>> # fetch older submssion 1492 | >>> ids = api.submission_ids(model_id) 1493 | >>> import random; submission_id = random.choice(ids)["id"] 1494 | >>> api.download_submission(submission_id=submission_id) 1495 | """ 1496 | msg = "You need to provide one of `model_id` and `submission_id" 1497 | assert model_id or submission_id, msg 1498 | auth_query = ''' 1499 | query($id: String) { 1500 | submissionDownloadAuth(id: $id) { 1501 | filename 1502 | url 1503 | } 1504 | }''' 1505 | if not submission_id: 1506 | ids = self.submission_ids(model_id) 1507 | submission_id = max(ids, key=lambda x: x['insertedAt'])["id"] 1508 | 1509 | data = self.raw_query( 1510 | auth_query, {'id': submission_id}, 1511 | authorization=True)['data']["submissionDownloadAuth"] 1512 | if dest_path is None: 1513 | dest_path = data["filename"] 1514 | path = utils.download_file(data["url"], dest_path) 1515 | return path 1516 | 1517 | def upload_predictions(self, file_path: str = "predictions.csv", 1518 | model_id: str | None = None, 1519 | df: pd.DataFrame | None = None, 1520 | data_datestamp: int | None = None, 1521 | timeout: Union[None, float, Tuple[float, float]] = (10, 600) 1522 | ) -> str: 1523 | """Upload predictions from file. 1524 | Will read TRIGGER_ID from the environment if this model is enabled with 1525 | a Numerai Compute cluster setup by Numerai CLI. 1526 | 1527 | Args: 1528 | file_path (str): CSV file with predictions that will get uploaded 1529 | model_id (str): Target model UUID (required for accounts with 1530 | multiple models) 1531 | df (pandas.DataFrame): pandas DataFrame to upload, if function is 1532 | given df and file_path, df will be uploaded. 1533 | data_datestamp (int): Data lag, in case submission is done using 1534 | data from the previous day(s). 1535 | timeout (float|tuple(float,float)): waiting time (connection timeout, 1536 | read timeout) 1537 | 1538 | Returns: 1539 | str: submission_id 1540 | 1541 | Example: 1542 | >>> api = NumerAPI(secret_key="..", public_id="..") 1543 | >>> model_id = api.get_models()['uuazed'] 1544 | >>> api.upload_predictions("prediction.cvs", model_id=model_id) 1545 | '93c46857-fed9-4594-981e-82db2b358daf' 1546 | >>> # upload from pandas DataFrame directly: 1547 | >>> api.upload_predictions(df=predictions_df, model_id=model_id) 1548 | """ 1549 | self.logger.info("uploading predictions...") 1550 | 1551 | # write the pandas DataFrame as a binary buffer if provided 1552 | buffer_csv = None 1553 | 1554 | if df is not None: 1555 | buffer_csv = BytesIO(df.to_csv(index=False).encode()) 1556 | buffer_csv.name = file_path 1557 | 1558 | upload_auth = self._upload_auth( 1559 | 'submission_upload_auth', file_path, self.tournament_id, model_id) 1560 | 1561 | # get compute id if available and pass it along 1562 | headers = {"x_compute_id": os.getenv("NUMERAI_COMPUTE_ID")} 1563 | with open(file_path, 'rb') if df is None else buffer_csv as file: 1564 | requests.put( 1565 | upload_auth['url'], data=file.read(), headers=headers, 1566 | timeout=timeout) 1567 | create_query = ''' 1568 | mutation($filename: String! 1569 | $tournament: Int! 1570 | $modelId: String 1571 | $triggerId: String, 1572 | $dataDatestamp: Int) { 1573 | create_submission(filename: $filename 1574 | tournament: $tournament 1575 | modelId: $modelId 1576 | triggerId: $triggerId 1577 | source: "numerapi" 1578 | dataDatestamp: $dataDatestamp) { 1579 | id 1580 | } 1581 | } 1582 | ''' 1583 | arguments = {'filename': upload_auth['filename'], 1584 | 'tournament': self.tournament_id, 1585 | 'modelId': model_id, 1586 | 'triggerId': os.getenv('TRIGGER_ID', None), 1587 | 'dataDatestamp': data_datestamp} 1588 | create = self.raw_query(create_query, arguments, authorization=True) 1589 | submission_id = create['data']['create_submission']['id'] 1590 | return submission_id 1591 | -------------------------------------------------------------------------------- /numerapi/cli.py: -------------------------------------------------------------------------------- 1 | """ Access the numerai API via command line""" 2 | 3 | import json 4 | import datetime 5 | import decimal 6 | 7 | import click 8 | 9 | import numerapi 10 | 11 | napi = numerapi.NumerAPI() 12 | 13 | 14 | class CommonJSONEncoder(json.JSONEncoder): 15 | """ 16 | Common JSON Encoder 17 | json.dumps(jsonString, cls=CommonJSONEncoder) 18 | """ 19 | def default(self, o): 20 | # Encode: Decimal 21 | if isinstance(o, decimal.Decimal): 22 | return str(o) 23 | # Encode: Date & Datetime 24 | if isinstance(o, (datetime.date, datetime.datetime)): 25 | return o.isoformat() 26 | 27 | return None 28 | 29 | 30 | def prettify(stuff): 31 | """prettify json""" 32 | return json.dumps(stuff, cls=CommonJSONEncoder, indent=4) 33 | 34 | 35 | @click.group() 36 | def cli(): 37 | """Wrapper around the Numerai API""" 38 | 39 | 40 | @cli.command() 41 | @click.option('--round_num', 42 | help='round you are interested in.defaults to the current round') 43 | def list_datasets(round_num): 44 | """List of available data files""" 45 | click.echo(prettify(napi.list_datasets(round_num=round_num))) 46 | 47 | 48 | @cli.command() 49 | @click.option( 50 | '--round_num', 51 | help='round you are interested in.defaults to the current round') 52 | @click.option( 53 | '--filename', help='file to be downloaded') 54 | @click.option( 55 | '--dest_path', 56 | help='complate destination path, defaults to the name of the source file') 57 | def download_dataset(round_num, filename="numerai_live_data.parquet", 58 | dest_path=None): 59 | """Download specified file for the given round""" 60 | click.echo("WARNING to download the old data use `download-dataset-old`") 61 | click.echo(napi.download_dataset( 62 | round_num=round_num, filename=filename, dest_path=dest_path)) 63 | 64 | 65 | @cli.command() 66 | @click.option('--tournament', default=8, 67 | help='The ID of the tournament, defaults to 8') 68 | def competitions(tournament=8): 69 | """Retrieves information about all competitions""" 70 | click.echo(prettify(napi.get_competitions(tournament=tournament))) 71 | 72 | 73 | @cli.command() 74 | @click.option('--tournament', default=8, 75 | help='The ID of the tournament, defaults to 8') 76 | def current_round(tournament=8): 77 | """Get number of the current active round.""" 78 | click.echo(napi.get_current_round(tournament=tournament)) 79 | 80 | 81 | @cli.command() 82 | @click.option('--limit', default=20, 83 | help='Number of items to return, defaults to 20') 84 | @click.option('--offset', default=0, 85 | help='Number of items to skip, defaults to 0') 86 | def leaderboard(limit=20, offset=0): 87 | """Get the leaderboard.""" 88 | click.echo(prettify(napi.get_leaderboard(limit=limit, offset=offset))) 89 | 90 | 91 | @cli.command() 92 | @click.option('--tournament', type=int, default=None, 93 | help='filter by ID of the tournament, defaults to None') 94 | @click.option('--round_num', type=int, default=None, 95 | help='filter by round number, defaults to None') 96 | @click.option( 97 | '--model_id', type=str, default=None, 98 | help="An account model UUID (required for accounts with multiple models") 99 | def submission_filenames(round_num, tournament, model_id): 100 | """Get filenames of your submissions""" 101 | click.echo(prettify( 102 | napi.get_submission_filenames(tournament, round_num, model_id))) 103 | 104 | 105 | @cli.command() 106 | @click.option('--hours', default=12, 107 | help='timeframe to consider, defaults to 12') 108 | def check_new_round(hours=12): 109 | """Check if a new round has started within the last `hours`.""" 110 | click.echo(int(napi.check_new_round(hours=hours))) 111 | 112 | 113 | @cli.command() 114 | def account(): 115 | """Get all information about your account!""" 116 | click.echo(prettify(napi.get_account())) 117 | 118 | 119 | @cli.command() 120 | @click.option('--tournament', default=8, 121 | help='The ID of the tournament, defaults to 8') 122 | def models(tournament): 123 | """Get map of account models!""" 124 | click.echo(prettify(napi.get_models(tournament))) 125 | 126 | 127 | @cli.command() 128 | @click.argument("username") 129 | def profile(username): 130 | """Fetch the public profile of a user.""" 131 | click.echo(prettify(napi.public_user_profile(username))) 132 | 133 | 134 | @cli.command() 135 | @click.argument("username") 136 | def daily_model_performances(username): 137 | """Fetch daily performance of a model.""" 138 | click.echo(prettify(napi.daily_model_performances(username))) 139 | 140 | 141 | @cli.command() 142 | def transactions(): 143 | """List all your deposits and withdrawals.""" 144 | click.echo(prettify(napi.wallet_transactions())) 145 | 146 | 147 | @cli.command() 148 | @click.option('--tournament', default=8, 149 | help='The ID of the tournament, defaults to 8') 150 | @click.option( 151 | '--model_id', type=str, default=None, 152 | help="An account model UUID (required for accounts with multiple models") 153 | @click.argument('path', type=click.Path(exists=True)) 154 | def submit(path, tournament, model_id): 155 | """Upload predictions from file.""" 156 | click.echo(napi.upload_predictions( 157 | path, tournament, model_id)) 158 | 159 | 160 | @cli.command() 161 | @click.argument("username") 162 | def stake_get(username): 163 | """Get stake value of a user.""" 164 | click.echo(napi.stake_get(username)) 165 | 166 | 167 | @cli.command() 168 | @click.option( 169 | '--model_id', type=str, default=None, 170 | help="An account model UUID (required for accounts with multiple models") 171 | def stake_drain(model_id): 172 | """Completely remove your stake.""" 173 | click.echo(napi.stake_drain(model_id)) 174 | 175 | 176 | @cli.command() 177 | @click.argument("nmr") 178 | @click.option( 179 | '--model_id', type=str, default=None, 180 | help="An account model UUID (required for accounts with multiple models") 181 | def stake_decrease(nmr, model_id): 182 | """Decrease your stake by `value` NMR.""" 183 | click.echo(napi.stake_decrease(nmr, model_id)) 184 | 185 | 186 | @cli.command() 187 | @click.argument("nmr") 188 | @click.option( 189 | '--model_id', type=str, default=None, 190 | help="An account model UUID (required for accounts with multiple models") 191 | def stake_increase(nmr, model_id): 192 | """Increase your stake by `value` NMR.""" 193 | click.echo(napi.stake_increase(nmr, model_id)) 194 | 195 | 196 | @cli.command() 197 | def version(): 198 | """Installed numerapi version.""" 199 | print(numerapi.__version__) 200 | 201 | 202 | if __name__ == "__main__": 203 | cli() 204 | -------------------------------------------------------------------------------- /numerapi/cryptoapi.py: -------------------------------------------------------------------------------- 1 | """API for Numerai Crypto""" 2 | 3 | from numerapi import base_api 4 | 5 | class CryptoAPI(base_api.Api): 6 | """"API for Numerai Crypto""" 7 | 8 | def __init__(self, *args, **kwargs): 9 | base_api.Api.__init__(self, *args, **kwargs) 10 | self.tournament_id = 12 11 | -------------------------------------------------------------------------------- /numerapi/numerapi.py: -------------------------------------------------------------------------------- 1 | """API for Numerai Classic""" 2 | 3 | import decimal 4 | from typing import List, Dict 5 | 6 | from numerapi import utils 7 | from numerapi import base_api 8 | 9 | 10 | class NumerAPI(base_api.Api): 11 | """Wrapper around the Numerai API 12 | 13 | Automatically download and upload data for the Numerai machine learning 14 | competition. 15 | 16 | This library is a Python client to the Numerai API. The interface is 17 | implemented in Python and tournamentallows downloading the training data, 18 | uploading predictions, accessing user, submission and competitions 19 | information and much more. 20 | """ 21 | 22 | def __init__(self, *args, **kwargs): 23 | base_api.Api.__init__(self, *args, **kwargs) 24 | self.tournament_id = 8 25 | 26 | def get_competitions(self, tournament=8): 27 | """Retrieves information about all competitions 28 | 29 | Args: 30 | tournament (int, optional): ID of the tournament, defaults to 8 31 | 32 | Returns: 33 | list of dicts: list of rounds 34 | 35 | Each round's dict contains the following items: 36 | 37 | * number (`int`) 38 | * openTime (`datetime`) 39 | * resolveTime (`datetime`) 40 | * resolvedGeneral (`bool`) 41 | * resolvedStaking (`bool`) 42 | 43 | Example: 44 | >>> NumerAPI().get_competitions() 45 | [ 46 | {'number': 71, 47 | 'openTime': datetime.datetime(2017, 8, 31, 0, 0), 48 | 'resolveTime': datetime.datetime(2017, 9, 27, 21, 0), 49 | 'resolvedGeneral': True, 50 | 'resolvedStaking': True, 51 | }, 52 | .. 53 | ] 54 | """ 55 | self.logger.info("getting rounds...") 56 | 57 | query = ''' 58 | query($tournament: Int!) { 59 | rounds(tournament: $tournament) { 60 | number 61 | resolveTime 62 | openTime 63 | resolvedGeneral 64 | resolvedStaking 65 | } 66 | } 67 | ''' 68 | arguments = {'tournament': tournament} 69 | result = self.raw_query(query, arguments) 70 | rounds = result['data']['rounds'] 71 | # convert datetime strings to datetime.datetime objects 72 | for rnd in rounds: 73 | utils.replace(rnd, "openTime", utils.parse_datetime_string) 74 | utils.replace(rnd, "resolveTime", utils.parse_datetime_string) 75 | return rounds 76 | 77 | def get_submission_filenames(self, tournament=None, round_num=None, 78 | model_id=None) -> List[Dict]: 79 | """Get filenames of the submission of the user. 80 | 81 | Args: 82 | tournament (int): optionally filter by ID of the tournament 83 | round_num (int): optionally filter round number 84 | model_id (str): Target model UUID (required for accounts with 85 | multiple models) 86 | 87 | Returns: 88 | list: list of user filenames (`dict`) 89 | 90 | Each filenames in the list as the following structure: 91 | 92 | * filename (`str`) 93 | * round_num (`int`) 94 | * tournament (`int`) 95 | 96 | Example: 97 | >>> api = NumerAPI(secret_key="..", public_id="..") 98 | >>> model = api.get_models()['uuazed'] 99 | >>> api.get_submission_filenames(3, 111, model) 100 | [{'filename': 'model57-dMpHpYMPIUAF.csv', 101 | 'round_num': 111, 102 | 'tournament': 3}] 103 | 104 | """ 105 | query = """ 106 | query($modelId: String) { 107 | model(modelId: $modelId) { 108 | submissions { 109 | filename 110 | selected 111 | round { 112 | tournament 113 | number 114 | } 115 | } 116 | } 117 | } 118 | """ 119 | arguments = {'modelId': model_id} 120 | data = self.raw_query( 121 | query, arguments, authorization=True)['data']['model'] 122 | 123 | filenames = [{"round_num": item['round']['number'], 124 | "tournament": item['round']['tournament'], 125 | "filename": item['filename']} 126 | for item in data['submissions'] if item['selected']] 127 | 128 | if round_num is not None: 129 | filenames = [f for f in filenames if f['round_num'] == round_num] 130 | if tournament is not None: 131 | filenames = [f for f in filenames if f['tournament'] == tournament] 132 | filenames.sort(key=lambda f: (f['round_num'], f['tournament'])) 133 | return filenames 134 | 135 | def get_leaderboard(self, limit: int = 50, offset: int = 0) -> List[Dict]: 136 | """Get the current model leaderboard 137 | 138 | Args: 139 | limit (int): number of items to return (optional, defaults to 50) 140 | offset (int): number of items to skip (optional, defaults to 0) 141 | 142 | Returns: 143 | list of dicts: list of leaderboard entries 144 | 145 | Each dict contains the following items: 146 | 147 | * username (`str`) 148 | * rank (`int`) 149 | * nmrStaked (`decimal.Decimal`) 150 | * corr20Rep (`float`) 151 | * corj60Rep (`float`) 152 | * fncRep (`float`) 153 | * fncV3Rep (`float`) 154 | * tcRep (`float`) 155 | * mmcRep (`float`) 156 | * bmcRep (`float`) 157 | * team (`bool`) 158 | * return_1_day (`float`) 159 | * return_52_day (`float`) 160 | * return_13_day (`float`) 161 | 162 | Example: 163 | >>> numerapi.NumerAPI().get_leaderboard(1) 164 | [{'username': 'anton', 165 | 'rank': 143, 166 | 'nmrStaked': Decimal('12'), 167 | ... 168 | }] 169 | 170 | """ 171 | query = ''' 172 | query($limit: Int! 173 | $offset: Int!) { 174 | v2Leaderboard(limit: $limit 175 | offset: $offset) { 176 | nmrStaked 177 | rank 178 | username 179 | corr20Rep 180 | corr20V2Rep 181 | corj60Rep 182 | fncRep 183 | fncV3Rep 184 | tcRep 185 | mmcRep 186 | bmcRep 187 | team 188 | return_1_day 189 | return_52_weeks 190 | return_13_weeks 191 | } 192 | } 193 | ''' 194 | 195 | arguments = {'limit': limit, 'offset': offset} 196 | data = self.raw_query(query, arguments)['data']['v2Leaderboard'] 197 | for item in data: 198 | utils.replace(item, "nmrStaked", utils.parse_float_string) 199 | return data 200 | 201 | def stake_set(self, nmr, model_id: str) -> Dict: 202 | """Set stake to value by decreasing or increasing your current stake 203 | 204 | Args: 205 | nmr (float or str): amount of NMR you want to stake 206 | model_id (str): model_id for where you want to stake 207 | 208 | Returns: 209 | dict: stake information with the following content: 210 | 211 | * insertedAt (`datetime`) 212 | * status (`str`) 213 | * txHash (`str`) 214 | * value (`decimal.Decimal`) 215 | * source (`str`) 216 | * to (`str`) 217 | * from (`str`) 218 | * posted (`bool`) 219 | 220 | Example: 221 | >>> api = NumerAPI(secret_key="..", public_id="..") 222 | >>> api.stake_set(10) 223 | {'from': None, 224 | 'insertedAt': None, 225 | 'status': None, 226 | 'txHash': '0x76519...2341ca0', 227 | 'from': '', 228 | 'to': '', 229 | 'posted': True, 230 | 'value': '10'} 231 | """ 232 | # fetch current stake 233 | modelname = self.modelid_to_modelname(model_id) 234 | current = self.stake_get(modelname) 235 | # convert everything to decimals 236 | if current is None: 237 | current = decimal.Decimal(0) 238 | else: 239 | current = decimal.Decimal(str(current)) 240 | if not isinstance(nmr, decimal.Decimal): 241 | nmr = decimal.Decimal(str(nmr)) 242 | # update stake! 243 | if nmr < current: 244 | return self.stake_decrease(current - nmr, model_id) 245 | if nmr > current: 246 | return self.stake_increase(nmr - current, model_id) 247 | self.logger.info("Stake already at desired value. Nothing to do.") 248 | return None 249 | 250 | def stake_get(self, modelname: str) -> float: 251 | """Get your current stake amount. 252 | 253 | Args: 254 | modelname (str) 255 | 256 | Returns: 257 | float: current stake (including projected NMR earnings from open 258 | rounds) 259 | 260 | Example: 261 | >>> api = NumerAPI() 262 | >>> api.stake_get("uuazed") 263 | 1.1 264 | """ 265 | query = """ 266 | query($modelname: String!) { 267 | v3UserProfile(modelName: $modelname) { 268 | stakeValue 269 | } 270 | } 271 | """ 272 | arguments = {'modelname': modelname} 273 | data = self.raw_query(query, arguments)['data']['v3UserProfile'] 274 | return data['stakeValue'] 275 | 276 | def public_user_profile(self, username: str) -> Dict: 277 | """Fetch the public profile of a user. 278 | 279 | Args: 280 | username (str) 281 | 282 | Returns: 283 | dict: user profile including the following fields: 284 | * username (`str`) 285 | * startDate (`datetime`) 286 | * id (`string`) 287 | * bio (`str`) 288 | * nmrStaked (`float`) 289 | 290 | Example: 291 | >>> api = NumerAPI() 292 | >>> api.public_user_profile("integration_test") 293 | {'bio': 'The official example model. Submits example predictions.', 294 | 'id': '59de8728-38e5-45bd-a3d5-9d4ad649dd3f', 295 | 'startDate': datetime.datetime( 296 | 2018, 6, 6, 17, 33, 21, tzinfo=tzutc()), 297 | 'nmrStaked': '57.582371875005243780', 298 | 'username': 'integration_test'} 299 | 300 | """ 301 | query = """ 302 | query($model_name: String!) { 303 | v3UserProfile(model_name: $model_name) { 304 | id 305 | startDate 306 | username 307 | bio 308 | nmrStaked 309 | } 310 | } 311 | """ 312 | arguments = {'model_name': username} 313 | data = self.raw_query(query, arguments)['data']['v3UserProfile'] 314 | # convert strings to python objects 315 | utils.replace(data, "startDate", utils.parse_datetime_string) 316 | return data 317 | 318 | def daily_model_performances(self, username: str) -> List[Dict]: 319 | """Fetch daily performance of a user. 320 | 321 | Args: 322 | username (str) 323 | 324 | Returns: 325 | list of dicts: list of daily model performance entries 326 | 327 | For each entry in the list, there is a dict with the following 328 | content: 329 | 330 | * date (`datetime`) 331 | * corrRep (`float` or None) 332 | * corrRank (`int`) 333 | * mmcRep (`float` or None) 334 | * mmcRank (`int`) 335 | * fncRep (`float` or None) 336 | * fncRank (`int`) 337 | * fncV3Rep (`float` or None) 338 | * fncV3Rank (`int`) 339 | * tcRep (`float` or None) 340 | * tcRank (`int`) 341 | 342 | Example: 343 | >>> api = NumerAPI() 344 | >>> api.daily_model_performances("uuazed") 345 | [{'corrRank': 485, 346 | 'corrRep': 0.027951873730771848, 347 | 'date': datetime.datetime(2021, 9, 14, 0, 0, tzinfo=tzutc()), 348 | 'fncRank': 1708, 349 | 'fncRep': 0.014548700790462122, 350 | 'tcRank': 1708, 351 | 'tcRep': 0.014548700790462122, 352 | 'fncV3Rank': 1708, 353 | 'fncV3Rep': 0.014548700790462122, 354 | 'mmcRank': 508, 355 | 'mmcRep': 0.005321406467445256}, 356 | ... 357 | ] 358 | """ 359 | query = """ 360 | query($username: String!) { 361 | v3UserProfile(modelName: $username) { 362 | dailyModelPerformances { 363 | date 364 | corrRep 365 | corrRank 366 | mmcRep 367 | mmcRank 368 | fncRep 369 | fncRank 370 | fncV3Rep 371 | fncV3Rank 372 | tcRep 373 | tcRank 374 | } 375 | } 376 | } 377 | """ 378 | arguments = {'username': username} 379 | data = self.raw_query(query, arguments)['data']['v3UserProfile'] 380 | performances = data['dailyModelPerformances'] 381 | # convert strings to python objects 382 | for perf in performances: 383 | utils.replace(perf, "date", utils.parse_datetime_string) 384 | return performances 385 | -------------------------------------------------------------------------------- /numerapi/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uuazed/numerapi/d81dabbb52b500c85637fcff8b2380207034a8d5/numerapi/py.typed -------------------------------------------------------------------------------- /numerapi/signalsapi.py: -------------------------------------------------------------------------------- 1 | """API for Numerai Signals""" 2 | 3 | from typing import List, Dict, Tuple, Union 4 | import os 5 | import decimal 6 | from io import BytesIO 7 | 8 | import requests 9 | import pandas as pd 10 | 11 | from numerapi import base_api 12 | from numerapi import utils 13 | 14 | 15 | class SignalsAPI(base_api.Api): 16 | """"API for Numerai Signals""" 17 | 18 | def __init__(self, *args, **kwargs): 19 | base_api.Api.__init__(self, *args, **kwargs) 20 | self.tournament_id = 11 21 | 22 | def get_leaderboard(self, limit: int = 50, offset: int = 0) -> List[Dict]: 23 | """Get the current Numerai Signals leaderboard 24 | Args: 25 | limit (int): number of items to return (optional, defaults to 50) 26 | offset (int): number of items to skip (optional, defaults to 0) 27 | Returns: 28 | list of dicts: list of leaderboard entries 29 | Each dict contains the following items: 30 | * username (`str`) 31 | * sharpe (`float`) 32 | * rank (`int`) 33 | * prevRank (`int`) 34 | * today (`float`) 35 | * mmc (`float`) 36 | * mmcRank (`int`) 37 | * icRep (`float`) 38 | * icRank (`int`) 39 | * tcRep (`float`) 40 | * tcRank (`int`) 41 | * nmrStaked (`float`) 42 | Example: 43 | >>> numerapi.SignalsAPI().get_leaderboard(1) 44 | [{'prevRank': 1, 45 | 'rank': 1, 46 | 'sharpe': 2.3, 47 | 'today': 0.01321, 48 | 'username': 'floury_kerril_moodle', 49 | 'mmc': -0.0101202715, 50 | 'mmcRank': 30, 51 | 'nmrStaked': 13.0, 52 | 'icRep': -0.0101202715, 53 | 'icRank': 30, 54 | .. 55 | }] 56 | """ 57 | query = ''' 58 | query($limit: Int! 59 | $offset: Int!) { 60 | signalsLeaderboard(limit: $limit 61 | offset: $offset) { 62 | prevRank 63 | rank 64 | sharpe 65 | today 66 | username 67 | mmc 68 | mmcRank 69 | nmrStaked 70 | icRank 71 | icRep 72 | tcRep 73 | tcRank 74 | } 75 | } 76 | ''' 77 | 78 | arguments = {'limit': limit, 'offset': offset} 79 | data = self.raw_query(query, arguments)['data']['signalsLeaderboard'] 80 | return data 81 | 82 | def upload_predictions(self, file_path: str = "predictions.csv", 83 | model_id: str | None = None, 84 | df: pd.DataFrame | None = None, 85 | data_datestamp: int | None = None, 86 | timeout: Union[None, float, Tuple[float, float]] = (10, 600) 87 | ) -> str: 88 | """Upload predictions from file. 89 | Will read TRIGGER_ID from the environment if this model is enabled with 90 | a Numerai Compute cluster setup by Numerai CLI. 91 | 92 | Args: 93 | file_path (str): CSV file with predictions that will get uploaded 94 | model_id (str): Target model UUID (required for accounts 95 | with multiple models) 96 | df (pandas.DataFrame): Pandas DataFrame to upload, if function is 97 | given df and file_path, df will be uploaded 98 | timeout (float|tuple(float,float)): waiting time (connection timeout, 99 | read timeout) 100 | 101 | Returns: 102 | str: submission_id 103 | 104 | Example: 105 | >>> api = SignalsAPI(secret_key="..", public_id="..") 106 | >>> model_id = api.get_models()['uuazed'] 107 | >>> api.upload_predictions("prediction.cvs", model_id=model_id) 108 | '93c46857-fed9-4594-981e-82db2b358daf' 109 | 110 | >>> # upload directly from a pandas DataFrame: 111 | >>> api.upload_predictions(df = predictions_df, model_id=model_id) 112 | """ 113 | self.logger.info("uploading predictions...") 114 | 115 | # write the pandas DataFrame as a binary buffer if provided 116 | buffer_csv = None 117 | 118 | if df is not None: 119 | buffer_csv = BytesIO(df.to_csv(index=False).encode()) 120 | buffer_csv.name = file_path 121 | 122 | upload_auth = self._upload_auth( 123 | 'submissionUploadSignalsAuth', file_path, 124 | self.tournament_id, model_id) 125 | 126 | # get compute id if available and pass it along 127 | headers = {"x_compute_id": os.getenv("NUMERAI_COMPUTE_ID")} 128 | 129 | with open(file_path, 'rb') if df is None else buffer_csv as file: 130 | requests.put(upload_auth['url'], data=file.read(), 131 | headers=headers, timeout=timeout) 132 | create_query = ''' 133 | mutation($filename: String! 134 | $modelId: String 135 | $triggerId: String 136 | $dataDatestamp: Int) { 137 | createSignalsSubmission(filename: $filename 138 | modelId: $modelId 139 | triggerId: $triggerId 140 | source: "numerapi" 141 | dataDatestamp: $dataDatestamp) { 142 | id 143 | firstEffectiveDate 144 | } 145 | } 146 | ''' 147 | arguments = {'filename': upload_auth['filename'], 148 | 'modelId': model_id, 149 | 'triggerId': os.getenv('TRIGGER_ID', None), 150 | 'dataDatestamp': data_datestamp} 151 | create = self.raw_query(create_query, arguments, authorization=True) 152 | return create['data']['createSignalsSubmission']['id'] 153 | 154 | def public_user_profile(self, username: str) -> Dict: 155 | """Fetch the public Numerai Signals profile of a user. 156 | 157 | Args: 158 | username (str) 159 | 160 | Returns: 161 | dict: user profile including the following fields: 162 | 163 | * username (`str`) 164 | * startDate (`datetime`) 165 | * id (`string`) 166 | * bio (`str`) 167 | * nmrStaked (`decimal.Decimal`) 168 | 169 | Example: 170 | >>> api = SignalsAPI() 171 | >>> api.public_user_profile("floury_kerril_moodle") 172 | {'bio': None, 173 | 'id': '635db2a4-bdc6-4e5d-b515-f5120392c8c9', 174 | 'startDate': datetime.datetime(2019, 3, 26, 0, 43), 175 | 'username': 'floury_kerril_moodle', 176 | 'nmrStaked': Decimal('14.630994874320760131')} 177 | 178 | """ 179 | query = """ 180 | query($username: String!) { 181 | v2SignalsProfile(modelName: $username) { 182 | id 183 | startDate 184 | username 185 | bio 186 | nmrStaked 187 | } 188 | } 189 | """ 190 | arguments = {'username': username} 191 | data = self.raw_query(query, arguments)['data']['v2SignalsProfile'] 192 | # convert strings to python objects 193 | utils.replace(data, "startDate", utils.parse_datetime_string) 194 | utils.replace(data, "nmrStaked", utils.parse_float_string) 195 | return data 196 | 197 | def daily_model_performances(self, username: str) -> List[Dict]: 198 | """Fetch daily Numerai Signals performance of a model. 199 | 200 | Args: 201 | username (str) 202 | 203 | Returns: 204 | list of dicts: list of daily user performance entries 205 | 206 | For each entry in the list, there is a dict with the following 207 | content: 208 | 209 | * date (`datetime`) 210 | * corrRank (`int`) 211 | * corrRep (`float` or None) 212 | * mmcRank (`int`) 213 | * mmcRep (`float` or None) 214 | * icRank (`int`) 215 | * icRep (`float` or None) 216 | * tcRank (`int`) 217 | * tcRep (`float` or None) 218 | * corr20dRank (`int`) 219 | * corr20dRep (`float` or None) 220 | * corr60Rank (`int`) 221 | * corr60Rep (`float` or None) 222 | * mmc20dRank (`int`) 223 | * mmc20dRep (`float` or None) 224 | 225 | Example: 226 | >>> api = SignalsAPI() 227 | >>> api.daily_model_performances("floury_kerril_moodle") 228 | [{'corrRank': 45, 229 | 'corrRep': -0.00010935616731632354, 230 | 'corr20dRank': None, 231 | 'corr20dRep': None, 232 | 'mmc20dRank': None, 233 | 'mmc20dRep': None, 234 | 'date': datetime.datetime(2020, 9, 18, 0, 0, tzinfo=tzutc()), 235 | 'mmcRank': 6, 236 | 'mmcRep': 0.0, 237 | 'icRank': 6, 238 | 'icRep': 0.0, 239 | ...}, 240 | ... 241 | ] 242 | """ 243 | query = """ 244 | query($username: String!) { 245 | v2SignalsProfile(modelName: $username) { 246 | dailyModelPerformances { 247 | date 248 | corrRank 249 | corrRep 250 | mmcRep 251 | mmcRank 252 | corr20dRep 253 | corr20dRank 254 | corr60Rep 255 | corr60Rank 256 | icRep 257 | icRank 258 | tcRank 259 | tcRep 260 | mmc20dRep 261 | mmc20dRank 262 | } 263 | } 264 | } 265 | """ 266 | arguments = {'username': username} 267 | data = self.raw_query(query, arguments)['data']['v2SignalsProfile'] 268 | performances = data['dailyModelPerformances'] 269 | # convert strings to python objects 270 | for perf in performances: 271 | utils.replace(perf, "date", utils.parse_datetime_string) 272 | return performances 273 | 274 | def ticker_universe(self) -> List[str]: 275 | """fetch universe of accepted tickers 276 | 277 | Returns: 278 | list of strings: list of currently accepted tickers 279 | 280 | Example: 281 | >>> SignalsAPI().ticker_universe() 282 | ["MSFT", "AMZN", "APPL", ...] 283 | """ 284 | path = self.download_dataset("signals/v1.0/live.parquet") 285 | return pd.read_parquet(path).numerai_ticker.tolist() 286 | 287 | def stake_get(self, username) -> decimal.Decimal: 288 | """get current stake for a given users 289 | 290 | Args: 291 | username (str) 292 | 293 | Returns: 294 | decimal.Decimal: current stake 295 | 296 | Example: 297 | >>> SignalsAPI().stake_get("uuazed") 298 | Decimal('14.63') 299 | """ 300 | data = self.public_user_profile(username) 301 | return data['totalStake'] 302 | -------------------------------------------------------------------------------- /numerapi/utils.py: -------------------------------------------------------------------------------- 1 | """ collection of utility functions""" 2 | 3 | import os 4 | import decimal 5 | import logging 6 | import time 7 | import datetime 8 | import uuid 9 | import json 10 | 11 | import dateutil.parser 12 | import requests 13 | import tqdm 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | def load_secrets() -> tuple: 19 | """load secrets from environment variables or dotenv file""" 20 | 21 | try: 22 | from dotenv import load_dotenv # pylint: disable-msg=import-outside-toplevel 23 | load_dotenv() 24 | except ImportError: 25 | pass 26 | 27 | public_id = os.getenv("NUMERAI_PUBLIC_ID") 28 | secret_key = os.getenv("NUMERAI_SECRET_KEY") 29 | 30 | return public_id, secret_key 31 | 32 | 33 | def parse_datetime_string(string: str) -> datetime.datetime | None: 34 | """try to parse string to datetime object""" 35 | if string is None: 36 | return None 37 | return dateutil.parser.parse(string) 38 | 39 | 40 | def parse_float_string(string: str) -> decimal.Decimal | None: 41 | """try to parse string to decimal.Decimal object""" 42 | if string is None: 43 | return None 44 | try: 45 | val = decimal.Decimal(string.replace(",", "")) 46 | except decimal.InvalidOperation: 47 | val = None 48 | return val 49 | 50 | 51 | def replace(dictionary: dict, key: str, function): 52 | """apply a function to dict item""" 53 | if dictionary is not None and key in dictionary: 54 | dictionary[key] = function(dictionary[key]) 55 | 56 | 57 | def download_file(url: str, dest_path: str, show_progress_bars: bool = True): 58 | """downloads a file and shows a progress bar. allow resuming a download""" 59 | file_size = 0 60 | req = requests.get(url, stream=True, timeout=600) 61 | req.raise_for_status() 62 | 63 | # Total size in bytes. 64 | total_size = int(req.headers.get('content-length', 0)) 65 | temp_path = dest_path + ".temp" 66 | 67 | if os.path.exists(dest_path): 68 | logger.info("target file already exists") 69 | file_size = os.stat(dest_path).st_size # File size in bytes 70 | if file_size == total_size: 71 | # Download complete 72 | logger.info("download complete") 73 | return dest_path 74 | 75 | if os.path.exists(temp_path): 76 | file_size = os.stat(temp_path).st_size # File size in bytes 77 | 78 | if file_size < total_size: 79 | # Download incomplete 80 | logger.info("resuming download") 81 | resume_header = {'Range': f'bytes={file_size}-'} 82 | req = requests.get(url, headers=resume_header, stream=True, 83 | verify=False, allow_redirects=True, timeout=600) 84 | else: 85 | # Error, delete file and restart download 86 | logger.error("deleting file and restarting") 87 | os.remove(temp_path) 88 | file_size = 0 89 | else: 90 | # File does not exist, starting download 91 | logger.info("starting download") 92 | 93 | # write dataset to file and show progress bar 94 | pbar = tqdm.tqdm(total=total_size, unit='B', unit_scale=True, 95 | desc=dest_path, disable=not show_progress_bars) 96 | # Update progress bar to reflect how much of the file is already downloaded 97 | pbar.update(file_size) 98 | with open(temp_path, "ab") as dest_file: 99 | for chunk in req.iter_content(1024): 100 | dest_file.write(chunk) 101 | pbar.update(1024) 102 | # move temp file to target destination 103 | os.replace(temp_path, dest_path) 104 | return dest_path 105 | 106 | 107 | def post_with_err_handling(url: str, body: str, headers: dict, 108 | *, timeout: int | None = None, 109 | retries: int = 3, delay: int = 1, backoff: int = 2 110 | ) -> dict: 111 | """send `post` request and handle (some) errors that might occur""" 112 | try: 113 | resp = requests.post(url, json=body, headers=headers, timeout=timeout) 114 | while 500 <= resp.status_code < 600 and retries > 1: 115 | time.sleep(delay) 116 | delay *= backoff 117 | retries -= 1 118 | resp = requests.post(url, json=body, 119 | headers=headers, timeout=timeout) 120 | resp.raise_for_status() 121 | return resp.json() 122 | 123 | except requests.exceptions.HTTPError as err: 124 | logger.error(f"Http Error: {err}") 125 | except requests.exceptions.ConnectionError as err: 126 | logger.error(f"Error Connecting: {err}") 127 | except requests.exceptions.Timeout as err: 128 | logger.error(f"Timeout Error: {err}") 129 | except requests.exceptions.RequestException as err: 130 | logger.error(f"Oops, something went wrong: {err}") 131 | except json.decoder.JSONDecodeError as err: 132 | logger.error(f"Did not receive a valid JSON: {err}") 133 | 134 | return {} 135 | 136 | 137 | def is_valid_uuid(val: str) -> bool: 138 | """ check if the given string is a valid UUID """ 139 | try: 140 | uuid.UUID(str(val)) 141 | return True 142 | except ValueError: 143 | return False 144 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | python-dateutil 3 | pytz 4 | tqdm>=4.29.1 5 | click>=7.0 6 | pandas>=1.1.0 7 | -------------------------------------------------------------------------------- /requirements_tests.txt: -------------------------------------------------------------------------------- 1 | pytest>=4.6 2 | pytest-cov 3 | codecov 4 | responses 5 | flake8 6 | pandas>=1.1.0 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | from setuptools import find_packages 3 | 4 | 5 | def load(path): 6 | return open(path, 'r').read() 7 | 8 | 9 | numerapi_version = '2.20.6' 10 | 11 | 12 | classifiers = [ 13 | "Development Status :: 5 - Production/Stable", 14 | "Environment :: Console", 15 | "Intended Audience :: Science/Research", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | "Topic :: Scientific/Engineering"] 21 | 22 | 23 | if __name__ == "__main__": 24 | setup( 25 | name="numerapi", 26 | version=numerapi_version, 27 | maintainer="uuazed", 28 | maintainer_email="uuazed@gmail.com", 29 | description="Automatically download and upload data for the Numerai machine learning competition", 30 | long_description=load('README.md'), 31 | long_description_content_type='text/markdown', 32 | url='https://github.com/uuazed/numerapi', 33 | platforms="OS Independent", 34 | classifiers=classifiers, 35 | license='MIT License', 36 | package_data={'numerapi': ['LICENSE', 'README.md', "py.typed"]}, 37 | packages=find_packages(exclude=['tests']), 38 | install_requires=["requests", "pytz", "python-dateutil", 39 | "tqdm>=4.29.1", "click>=7.0", "pandas>=1.1.0"], 40 | entry_points={ 41 | 'console_scripts': [ 42 | 'numerapi = numerapi.cli:cli' 43 | ] 44 | }, 45 | ) 46 | -------------------------------------------------------------------------------- /tests/test_base_api.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import os 3 | import pytest 4 | import responses 5 | 6 | from numerapi import base_api 7 | 8 | 9 | @pytest.fixture(scope='function', name="api") 10 | def api_fixture(): 11 | api = base_api.Api(verbosity='DEBUG') 12 | return api 13 | 14 | 15 | def test_NumerAPI(): 16 | # invalid log level should raise 17 | with pytest.raises(AttributeError): 18 | base_api.Api(verbosity="FOO") 19 | 20 | 21 | def test__login(api): 22 | # passing only one of public_id and secret_key is not enough 23 | api._login(public_id="foo", secret_key=None) 24 | assert api.token is None 25 | api._login(public_id=None, secret_key="bar") 26 | assert api.token is None 27 | # passing both works 28 | api._login(public_id="foo", secret_key="bar") 29 | assert api.token == ("foo", "bar") 30 | 31 | # using env variables 32 | os.environ["NUMERAI_SECRET_KEY"] = "key" 33 | os.environ["NUMERAI_PUBLIC_ID"] = "id" 34 | api._login() 35 | assert api.token == ("id", "key") 36 | 37 | 38 | def test_raw_query(api): 39 | query = "query {latestNmrPrice {priceUsd}}" 40 | result = api.raw_query(query) 41 | assert isinstance(result, dict) 42 | assert "data" in result 43 | 44 | 45 | @responses.activate 46 | def test_get_account(api): 47 | api.token = ("", "") 48 | account = {'apiTokens': [{'name': 'uploads', 49 | 'public_id': 'AAA', 50 | 'scopes': ['upload_submission']}, 51 | {'name': '_internal_default', 52 | 'public_id': 'BBB', 53 | 'scopes': ['upload_submission', 54 | 'read_submission_info', 55 | 'read_user_info']}, 56 | {'name': 'all', 57 | 'public_id': 'CCC', 58 | 'scopes': ['upload_submission', 59 | 'stake', 60 | 'read_submission_info', 61 | 'read_user_info']}], 62 | 'availableNmr': '1.010000000000000000', 63 | 'email': 'no-reply@eu83t4nncmxv3g2.xyz', 64 | 'id': '0c10a70a-a851-478f-a289-7a05fe397008', 65 | 'insertedAt': "2018-01-01 11:11:11", 66 | 'mfaEnabled': False, 67 | 'models': [{'id': '881778ad-2ee9-4fb0-82b4-7b7c0f7ce17d', 68 | 'name': 'model1', 69 | 'submissions': 70 | [{'filename': 'predictions-pPbLKSHGiR.csv', 71 | 'id': 'f2369b69-8c43-47aa-b4de-de3a9de5f52c'}], 72 | 'v2Stake': {'status': None, 'txHash': None}}, 73 | {'id': '881778ad-2ee9-4fb0-82b4-7b7c0f7ce17d', 74 | 'name': 'model2', 75 | 'submissions': 76 | [{'filename': 'predictions-pPbPOWQNGiR.csv', 77 | 'id': '46a62500-87c7-4d7c-98ad-b743037e8cfd'}], 78 | 'v2Stake': {'status': None, 'txHash': None}}], 79 | 'status': 'VERIFIED', 80 | 'username': 'username1', 81 | 'walletAddress': '0x0000000000000000000000000000'} 82 | 83 | data = {'data': {'account': account}} 84 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 85 | res = api.get_account() 86 | assert isinstance(res, dict) 87 | assert len(res.get('models')) == 2 88 | 89 | 90 | @responses.activate 91 | def test_get_models(api): 92 | api.token = ("", "") 93 | models_list = [ 94 | {"name": "model_x", "id": "95b0d9e2-c901-4f2b-9c98-24138b0bd706", 95 | "tournament": 0}, 96 | {"name": "model_y", "id": "2c6d63a4-013f-42d1-bbaf-bf35725d29f7", 97 | "tournament": 0}] 98 | data = {'data': {'account': {'models': models_list}}} 99 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 100 | models = api.get_models() 101 | assert sorted(models.keys()) == ['model_x', 'model_y'] 102 | assert sorted(models.values()) == ['2c6d63a4-013f-42d1-bbaf-bf35725d29f7', 103 | '95b0d9e2-c901-4f2b-9c98-24138b0bd706'] 104 | 105 | 106 | @responses.activate 107 | def test_set_submission_webhook(api): 108 | api.token = ("", "") 109 | data = { 110 | "data": { 111 | "setSubmissionWebhook": "true" 112 | } 113 | } 114 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 115 | res = api.set_submission_webhook( 116 | '2c6d63a4-013f-42d1-bbaf-bf35725d29f7', 117 | 'https://triggerurl' 118 | ) 119 | assert res 120 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from click.testing import CliRunner 4 | from unittest.mock import patch 5 | 6 | from numerapi import cli 7 | 8 | 9 | @pytest.fixture(scope='function', name="login") 10 | def login(): 11 | os.environ["NUMERAI_PUBLIC_ID"] = "foo" 12 | os.environ["NUMERAI_SECRET_KEY"] = "bar" 13 | yield None 14 | # teardown 15 | del os.environ["NUMERAI_PUBLIC_ID"] 16 | del os.environ["NUMERAI_SECRET_KEY"] 17 | 18 | 19 | @patch('numerapi.NumerAPI.download_dataset') 20 | def test_download_dataset(mocked): 21 | result = CliRunner().invoke(cli.download_dataset) 22 | # just testing if calling works fine 23 | assert result.exit_code == 0 24 | 25 | 26 | @patch('numerapi.NumerAPI.get_leaderboard') 27 | def test_leaderboard(mocked): 28 | result = CliRunner().invoke(cli.leaderboard) 29 | # just testing if calling works fine 30 | assert result.exit_code == 0 31 | 32 | 33 | @patch('numerapi.NumerAPI.get_competitions') 34 | def test_competitions(mocked): 35 | result = CliRunner().invoke(cli.competitions, ['--tournament', 1]) 36 | # just testing if calling works fine 37 | assert result.exit_code == 0 38 | 39 | 40 | @patch('numerapi.NumerAPI.get_current_round') 41 | def test_current_round(mocked): 42 | result = CliRunner().invoke(cli.current_round, ['--tournament', 1]) 43 | # just testing if calling works fine 44 | assert result.exit_code == 0 45 | 46 | 47 | @patch('numerapi.NumerAPI.get_submission_filenames') 48 | def test_submission_filenames(mocked): 49 | result = CliRunner().invoke(cli.submission_filenames, ['--tournament', 1]) 50 | # just testing if calling works fine 51 | assert result.exit_code == 0 52 | 53 | 54 | @patch('numerapi.NumerAPI.check_new_round') 55 | def test_check_new_round(mocked): 56 | result = CliRunner().invoke(cli.check_new_round) 57 | # just testing if calling works fine 58 | assert result.exit_code == 0 59 | 60 | 61 | @patch('numerapi.NumerAPI.get_account') 62 | def test_account(mocked, login): 63 | result = CliRunner().invoke(cli.account) 64 | # just testing if calling works fine 65 | assert result.exit_code == 0 66 | 67 | 68 | @patch('numerapi.NumerAPI.get_models') 69 | def test_models(mocked, login): 70 | result = CliRunner().invoke(cli.models) 71 | # just testing if calling works fine 72 | assert result.exit_code == 0 73 | 74 | 75 | @patch('numerapi.NumerAPI.wallet_transactions') 76 | def test_transactions(mocked): 77 | result = CliRunner().invoke(cli.transactions) 78 | # just testing if calling works fine 79 | assert result.exit_code == 0 80 | 81 | 82 | @patch('numerapi.NumerAPI.upload_predictions') 83 | def test_submit(mocked, login, tmpdir): 84 | path = tmpdir.join("somefilepath") 85 | path.write("content") 86 | result = CliRunner().invoke( 87 | cli.submit, 88 | [str(path), '--model_id', '31a42870-38b6-4435-ad49-18b987ff4148']) 89 | # just testing if calling works fine 90 | assert result.exit_code == 0 91 | 92 | 93 | def test_version(): 94 | result = CliRunner().invoke(cli.version) 95 | # just testing if calling works fine 96 | assert result.exit_code == 0 97 | -------------------------------------------------------------------------------- /tests/test_numerapi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import os 3 | import datetime 4 | import pytz 5 | import responses 6 | 7 | import pandas as pd 8 | 9 | import numerapi 10 | from numerapi import base_api 11 | 12 | 13 | @pytest.fixture(scope='function', name="api") 14 | def api_fixture(): 15 | api = numerapi.NumerAPI(verbosity='DEBUG') 16 | return api 17 | 18 | 19 | def test_get_competitions(api): 20 | res = api.get_competitions(tournament=1) 21 | assert isinstance(res, list) 22 | assert len(res) > 80 23 | 24 | 25 | def test_get_current_round(api): 26 | current_round = api.get_current_round() 27 | assert current_round >= 82 28 | 29 | 30 | @pytest.mark.parametrize("fun", ["get_account", "wallet_transactions"]) 31 | def test_unauthorized_requests(api, fun): 32 | with pytest.raises(ValueError) as err: 33 | # while this won't work because we are not authorized, it still tells 34 | # us if the remaining code works 35 | getattr(api, fun)() 36 | # error should warn about not being logged in. 37 | assert "API keys required for this action" in str(err.value) or \ 38 | "Your session is invalid or has expired." in str(err.value) 39 | 40 | 41 | def test_error_handling(api): 42 | # String instead of Int 43 | with pytest.raises(ValueError): 44 | api.get_competitions("foo") 45 | # unauthendicated request 46 | with pytest.raises(ValueError): 47 | # set wrong token 48 | api.token = ("foo", "bar") 49 | api.get_account() 50 | 51 | 52 | @responses.activate 53 | def test_upload_predictions(api, tmpdir): 54 | api.token = ("", "") 55 | # we need to mock 3 network calls: 1. auth 2. file upload and 3. submission 56 | data = {"data": {"submission_upload_auth": {"url": "https://uploadurl", 57 | "filename": "filename"}}} 58 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 59 | responses.add(responses.PUT, "https://uploadurl") 60 | data = {"data": {"create_submission": {"id": "1234"}}} 61 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 62 | 63 | path = tmpdir.join("somefilepath") 64 | path.write("content") 65 | submission_id = api.upload_predictions(str(path)) 66 | 67 | assert submission_id == "1234" 68 | assert len(responses.calls) == 3 69 | 70 | 71 | @responses.activate 72 | def test_upload_predictions_df(api): 73 | api.token = ("", "") 74 | data = {"data": { 75 | "submission_upload_auth": {"url": "https://uploadurl", 76 | "filename": "predictions.csv"}}} 77 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 78 | responses.add(responses.PUT, "https://uploadurl") 79 | data = {"data": {"create_submission": {"id": "12345"}}} 80 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 81 | 82 | df = pd.DataFrame.from_dict({"id": [], "prediction": []}) 83 | submission_id = api.upload_predictions(df=df) 84 | 85 | assert len(responses.calls) == 3 86 | assert submission_id == "12345" 87 | 88 | 89 | @responses.activate 90 | def test_check_new_round(api): 91 | open_time = datetime.datetime.utcnow().replace(tzinfo=pytz.utc) 92 | data = {"data": {"rounds": [{"openTime": open_time.isoformat()}]}} 93 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 94 | 95 | open_time = datetime.datetime(2000, 1, 1).replace(tzinfo=pytz.utc) 96 | data = {"data": {"rounds": [{"openTime": open_time.isoformat()}]}} 97 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 98 | 99 | # first example 100 | assert api.check_new_round() 101 | # second 102 | assert not api.check_new_round() 103 | -------------------------------------------------------------------------------- /tests/test_signalsapi.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import datetime 3 | import responses 4 | 5 | import pandas as pd 6 | 7 | import numerapi 8 | from numerapi import base_api 9 | 10 | 11 | @pytest.fixture(scope='function', name="api") 12 | def api_fixture(): 13 | api = numerapi.SignalsAPI(verbosity='DEBUG') 14 | return api 15 | 16 | 17 | def test_get_leaderboard(api): 18 | lb = api.get_leaderboard(1) 19 | assert len(lb) == 1 20 | 21 | 22 | @responses.activate 23 | def test_upload_predictions(api, tmpdir): 24 | api.token = ("", "") 25 | # we need to mock 3 network calls: 1. auth 2. file upload and 3. submission 26 | data = {"data": {"submissionUploadSignalsAuth": 27 | {"url": "https://uploadurl", "filename": "filename"}}} 28 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 29 | responses.add(responses.PUT, "https://uploadurl") 30 | data = {"data": {"createSignalsSubmission": {"id": "1234"}}} 31 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 32 | 33 | path = tmpdir.join("somefilepath") 34 | path.write("content") 35 | submission_id = api.upload_predictions(str(path)) 36 | 37 | assert submission_id == "1234" 38 | assert len(responses.calls) == 3 39 | 40 | #Test pandas.DataFrame version of upload_predictions 41 | @responses.activate 42 | def test_upload_predictions_df(api): 43 | api.token = ("", "") 44 | # we need to mock 3 network calls: 1. auth 2. file upload and 3. submission 45 | data = {"data": {"submissionUploadSignalsAuth": 46 | {"url": "https://uploadurl", "filename": "predictions.csv"}}} 47 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 48 | responses.add(responses.PUT, "https://uploadurl") 49 | data = {"data": {"createSignalsSubmission": {"id": "12345"}}} 50 | responses.add(responses.POST, base_api.API_TOURNAMENT_URL, json=data) 51 | 52 | df = pd.DataFrame.from_dict({"bloomberg_ticker":[],"signal":[]}) 53 | submission_id = api.upload_predictions(df = df) 54 | 55 | assert submission_id == "12345" 56 | assert len(responses.calls) == 3 57 | 58 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import datetime 3 | import os 4 | from dateutil.tz import tzutc 5 | import responses 6 | import requests 7 | import decimal 8 | from numerapi import utils 9 | 10 | 11 | def test_parse_datetime_string(): 12 | s = "2017-12-24T20:48:25.90349Z" 13 | t = datetime.datetime(2017, 12, 24, 20, 48, 25, 903490, tzinfo=tzutc()) 14 | assert utils.parse_datetime_string(s) == t 15 | assert utils.parse_datetime_string(None) is None 16 | 17 | 18 | def test_parse_float_string(): 19 | assert utils.parse_float_string(None) is None 20 | assert utils.parse_float_string("") is None 21 | assert utils.parse_float_string("1.23") == decimal.Decimal("1.23") 22 | assert utils.parse_float_string("12") == decimal.Decimal("12.0") 23 | assert utils.parse_float_string("1,000.0") == decimal.Decimal("1000.0") 24 | assert utils.parse_float_string("0.4") == decimal.Decimal("0.4") 25 | 26 | 27 | def test_replace(): 28 | d = None 29 | assert utils.replace(d, "a", float) is None 30 | # empty dict 31 | d = {} 32 | assert not utils.replace(d, "a", float) 33 | # normal case 34 | d = {"a": "1"} 35 | utils.replace(d, "a", float) 36 | assert d["a"] == 1.0 37 | 38 | 39 | @responses.activate 40 | def test_download_file(tmpdir): 41 | url = "https://someurl" 42 | responses.add(responses.GET, url) 43 | 44 | # convert to string to make python<3.6 happy 45 | path = str(tmpdir.join("somefilepath")) 46 | utils.download_file("https://someurl", path) 47 | assert os.path.exists(path) 48 | 49 | 50 | @responses.activate 51 | def test_post_with_err_handling(caplog): 52 | # unreachable 53 | responses.add(responses.POST, "https://someurl1", status=404) 54 | utils.post_with_err_handling("https://someurl1", None, None) 55 | assert 'Http Error' in caplog.text 56 | caplog.clear() 57 | 58 | # invalid response type 59 | responses.add(responses.POST, "https://someurl2") 60 | utils.post_with_err_handling("https://someurl2", None, None) 61 | assert 'Oops' in caplog.text 62 | caplog.clear() 63 | 64 | # timeout 65 | responses.add(responses.POST, "https://someurl3", 66 | body=requests.exceptions.Timeout()) 67 | utils.post_with_err_handling("https://someurl3", None, None) 68 | assert 'Timeout Error' in caplog.text 69 | caplog.clear() 70 | 71 | # API down 72 | responses.add(responses.POST, "https://someurl4", status=500) 73 | utils.post_with_err_handling("https://someurl4", None, None) 74 | assert 'Internal Server Error' in caplog.text 75 | # we don't want to try parsing the response 76 | assert 'Did not receive a valid JSON' not in caplog.text 77 | caplog.clear() --------------------------------------------------------------------------------