Skip to content

Commit ff16abd

Browse files
joshterrilluuazed
andauthored
added --tournament flag to cli, added timezone-aware dates to base api, added mmcMultiplier and roundPayoutFactor (#119)
* added --tournament flag to cli, added timezone-aware dates to base api * added mmcMultiplier and roundPayoutFactor * updated changelog --------- Co-authored-by: uuazed <34725120+uuazed@users.noreply.github.com>
1 parent f3281e1 commit ff16abd

File tree

7 files changed

+178
-67
lines changed

7 files changed

+178
-67
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
Notable changes to this project.
33

44
## [dev]
5+
- cli: allow selecting tournaments (Signals/Crypto) via `--tournament`
6+
- added `mmcMultiplier` and `roundPayoutFactor` to `round_model_performances_v2`
57
- more type hints
68
- 'round_model_performances_v2' - add 'roundPayoutFactor' and 'mmcMultiplier'
79

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ To get started with the cli interface, let's take a look at the help page:
128128
user Get all information about you!...
129129
version Installed numerapi version.
130130

131+
All CLI commands accept a `--tournament` option. It defaults to `8`
132+
(the classic tournament), but you can point the CLI at Signals (`11`) or
133+
Crypto (`12`) on a per-command basis, e.g. `numerapi list-datasets --tournament 11`.
134+
131135

132136
Each command has it's own help page, for example:
133137

@@ -137,7 +141,8 @@ Each command has it's own help page, for example:
137141
Upload predictions from file.
138142

139143
Options:
140-
--tournament INTEGER The ID of the tournament, defaults to 1
144+
--tournament INTEGER Tournament to target (8 classic, 11 signals, 12
145+
crypto) [default: 8]
141146
--model_id TEXT An account model UUID (required for accounts with
142147
multiple models
143148

numerapi/base_api.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -887,12 +887,12 @@ def round_model_performances_v2(self, model_id: str):
887887
roundResolved,
888888
roundTarget,
889889
submissionScores {
890-
date,
891-
day,
892-
displayName,
893-
payoutPending,
894-
payoutSettled,
895-
percentile,
890+
date
891+
day
892+
displayName
893+
payoutPending
894+
payoutSettled
895+
percentile
896896
value
897897
}
898898
}
@@ -1164,7 +1164,7 @@ def check_round_open(self) -> bool:
11641164
return False
11651165
open_time = utils.parse_datetime_string(raw["openTime"])
11661166
deadline = utils.parse_datetime_string(raw["closeStakingTime"])
1167-
now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
1167+
now = datetime.datetime.now(tz=pytz.utc)
11681168
is_open = open_time < now < deadline
11691169
return is_open
11701170

@@ -1200,8 +1200,8 @@ def check_new_round(self, hours: int = 12) -> bool:
12001200
return False
12011201
if raw is None:
12021202
return False
1203-
open_time = utils.parse_datetime_string(raw["openTime"])
1204-
now = datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
1203+
open_time = utils.parse_datetime_string(raw['openTime'])
1204+
now = datetime.datetime.now(tz=pytz.utc)
12051205
is_new_round = open_time > now - datetime.timedelta(hours=hours)
12061206
return is_new_round
12071207

numerapi/cli.py

Lines changed: 120 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,43 @@
88

99
import numerapi
1010

11-
napi = numerapi.NumerAPI()
11+
DEFAULT_TOURNAMENT = 8
12+
13+
14+
def _get_api(tournament: int):
15+
"""
16+
Return the correct API implementation for a tournament.
17+
18+
Classic (and any tournament other than Signals/Crypto) uses NumerAPI,
19+
Signals (11) uses SignalsAPI, and Crypto (12) uses CryptoAPI.
20+
"""
21+
if tournament == 11:
22+
return numerapi.SignalsAPI()
23+
if tournament == 12:
24+
return numerapi.CryptoAPI()
25+
api = numerapi.NumerAPI()
26+
api.tournament_id = tournament
27+
return api
28+
29+
30+
def _require_method(api, method_name: str, command_name: str):
31+
"""Ensure the requested command is supported for the selected tournament."""
32+
if not hasattr(api, method_name):
33+
raise click.ClickException(
34+
f"The '{command_name}' command is not available for tournament "
35+
f"{api.tournament_id}.")
36+
return getattr(api, method_name)
37+
38+
39+
def tournament_option(func):
40+
"""Reusable Click option for selecting a tournament."""
41+
return click.option(
42+
'--tournament',
43+
type=int,
44+
default=DEFAULT_TOURNAMENT,
45+
show_default=True,
46+
help="Tournament to target (8 classic, 11 signals, 12 crypto).",
47+
)(func)
1248

1349

1450
class CommonJSONEncoder(json.JSONEncoder):
@@ -38,159 +74,192 @@ def cli():
3874

3975

4076
@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):
77+
@click.option(
78+
'--round_num', type=int,
79+
help='round you are interested in. defaults to the current round')
80+
@tournament_option
81+
def list_datasets(round_num, tournament):
4482
"""List of available data files"""
45-
click.echo(prettify(napi.list_datasets(round_num=round_num)))
83+
api = _get_api(tournament)
84+
click.echo(prettify(api.list_datasets(round_num=round_num)))
4685

4786

4887
@cli.command()
4988
@click.option(
50-
'--round_num',
51-
help='round you are interested in.defaults to the current round')
89+
'--round_num', type=int,
90+
help='round you are interested in. defaults to the current round')
5291
@click.option(
53-
'--filename', help='file to be downloaded')
92+
'--filename', default="numerai_live_data.parquet", show_default=True,
93+
help='file to be downloaded')
5494
@click.option(
5595
'--dest_path',
56-
help='complate destination path, defaults to the name of the source file')
96+
help='complete destination path, defaults to the name of the source file')
97+
@tournament_option
5798
def download_dataset(round_num, filename="numerai_live_data.parquet",
58-
dest_path=None):
99+
dest_path=None, tournament=DEFAULT_TOURNAMENT):
59100
"""Download specified file for the given round"""
60101
click.echo("WARNING to download the old data use `download-dataset-old`")
61-
click.echo(napi.download_dataset(
102+
api = _get_api(tournament)
103+
click.echo(api.download_dataset(
62104
round_num=round_num, filename=filename, dest_path=dest_path))
63105

64106

65107
@cli.command()
66-
@click.option('--tournament', default=8,
67-
help='The ID of the tournament, defaults to 8')
68-
def competitions(tournament=8):
108+
@tournament_option
109+
def competitions(tournament=DEFAULT_TOURNAMENT):
69110
"""Retrieves information about all competitions"""
70-
click.echo(prettify(napi.get_competitions(tournament=tournament)))
111+
api = _get_api(tournament)
112+
method = _require_method(api, 'get_competitions', 'competitions')
113+
click.echo(prettify(method(tournament=tournament)))
71114

72115

73116
@cli.command()
74-
@click.option('--tournament', default=8,
75-
help='The ID of the tournament, defaults to 8')
76-
def current_round(tournament=8):
117+
@tournament_option
118+
def current_round(tournament=DEFAULT_TOURNAMENT):
77119
"""Get number of the current active round."""
78-
click.echo(napi.get_current_round(tournament=tournament))
120+
api = _get_api(tournament)
121+
click.echo(api.get_current_round(tournament=tournament))
79122

80123

81124
@cli.command()
82125
@click.option('--limit', default=20,
83126
help='Number of items to return, defaults to 20')
84127
@click.option('--offset', default=0,
85128
help='Number of items to skip, defaults to 0')
86-
def leaderboard(limit=20, offset=0):
129+
@tournament_option
130+
def leaderboard(limit=20, offset=0, tournament=DEFAULT_TOURNAMENT):
87131
"""Get the leaderboard."""
88-
click.echo(prettify(napi.get_leaderboard(limit=limit, offset=offset)))
132+
api = _get_api(tournament)
133+
method = _require_method(api, 'get_leaderboard', 'leaderboard')
134+
click.echo(prettify(method(limit=limit, offset=offset)))
89135

90136

91137
@cli.command()
92-
@click.option('--tournament', type=int, default=None,
93-
help='filter by ID of the tournament, defaults to None')
94138
@click.option('--round_num', type=int, default=None,
95139
help='filter by round number, defaults to None')
96140
@click.option(
97141
'--model_id', type=str, default=None,
98142
help="An account model UUID (required for accounts with multiple models")
143+
@tournament_option
99144
def submission_filenames(round_num, tournament, model_id):
100145
"""Get filenames of your submissions"""
146+
api = _get_api(tournament)
147+
method = _require_method(
148+
api, 'get_submission_filenames', 'submission-filenames')
101149
click.echo(prettify(
102-
napi.get_submission_filenames(tournament, round_num, model_id)))
150+
method(tournament=tournament, round_num=round_num, model_id=model_id)))
103151

104152

105153
@cli.command()
106154
@click.option('--hours', default=12,
107155
help='timeframe to consider, defaults to 12')
108-
def check_new_round(hours=12):
156+
@tournament_option
157+
def check_new_round(hours=12, tournament=DEFAULT_TOURNAMENT):
109158
"""Check if a new round has started within the last `hours`."""
110-
click.echo(int(napi.check_new_round(hours=hours)))
159+
api = _get_api(tournament)
160+
click.echo(int(api.check_new_round(hours=hours)))
111161

112162

113163
@cli.command()
114-
def account():
164+
@tournament_option
165+
def account(tournament=DEFAULT_TOURNAMENT):
115166
"""Get all information about your account!"""
116-
click.echo(prettify(napi.get_account()))
167+
api = _get_api(tournament)
168+
click.echo(prettify(api.get_account()))
117169

118170

119171
@cli.command()
120-
@click.option('--tournament', default=8,
121-
help='The ID of the tournament, defaults to 8')
122-
def models(tournament):
172+
@tournament_option
173+
def models(tournament=DEFAULT_TOURNAMENT):
123174
"""Get map of account models!"""
124-
click.echo(prettify(napi.get_models(tournament)))
175+
api = _get_api(tournament)
176+
click.echo(prettify(api.get_models(tournament)))
125177

126178

127179
@cli.command()
128180
@click.argument("username")
129-
def profile(username):
181+
@tournament_option
182+
def profile(username, tournament=DEFAULT_TOURNAMENT):
130183
"""Fetch the public profile of a user."""
131-
click.echo(prettify(napi.public_user_profile(username)))
184+
api = _get_api(tournament)
185+
method = _require_method(api, 'public_user_profile', 'profile')
186+
click.echo(prettify(method(username)))
132187

133188

134189
@cli.command()
135190
@click.argument("username")
136-
def daily_model_performances(username):
191+
@tournament_option
192+
def daily_model_performances(username, tournament=DEFAULT_TOURNAMENT):
137193
"""Fetch daily performance of a model."""
138-
click.echo(prettify(napi.daily_model_performances(username)))
194+
api = _get_api(tournament)
195+
method = _require_method(
196+
api, 'daily_model_performances', 'daily-model-performances')
197+
click.echo(prettify(method(username)))
139198

140199

141200
@cli.command()
142-
def transactions():
201+
@tournament_option
202+
def transactions(tournament=DEFAULT_TOURNAMENT):
143203
"""List all your deposits and withdrawals."""
144-
click.echo(prettify(napi.wallet_transactions()))
204+
api = _get_api(tournament)
205+
click.echo(prettify(api.wallet_transactions()))
145206

146207

147208
@cli.command()
148-
@click.option('--tournament', default=8,
149-
help='The ID of the tournament, defaults to 8')
150209
@click.option(
151210
'--model_id', type=str, default=None,
152211
help="An account model UUID (required for accounts with multiple models")
153212
@click.argument('path', type=click.Path(exists=True))
154-
def submit(path, tournament, model_id):
213+
@tournament_option
214+
def submit(path, model_id, tournament=DEFAULT_TOURNAMENT):
155215
"""Upload predictions from file."""
156-
click.echo(napi.upload_predictions(
157-
path, tournament, model_id))
216+
api = _get_api(tournament)
217+
click.echo(api.upload_predictions(path, model_id=model_id))
158218

159219

160220
@cli.command()
161221
@click.argument("username")
162-
def stake_get(username):
222+
@tournament_option
223+
def stake_get(username, tournament=DEFAULT_TOURNAMENT):
163224
"""Get stake value of a user."""
164-
click.echo(napi.stake_get(username))
225+
api = _get_api(tournament)
226+
method = _require_method(api, 'stake_get', 'stake-get')
227+
click.echo(method(username))
165228

166229

167230
@cli.command()
168231
@click.option(
169232
'--model_id', type=str, default=None,
170233
help="An account model UUID (required for accounts with multiple models")
171-
def stake_drain(model_id):
234+
@tournament_option
235+
def stake_drain(model_id, tournament=DEFAULT_TOURNAMENT):
172236
"""Completely remove your stake."""
173-
click.echo(napi.stake_drain(model_id))
237+
api = _get_api(tournament)
238+
click.echo(api.stake_drain(model_id))
174239

175240

176241
@cli.command()
177242
@click.argument("nmr")
178243
@click.option(
179244
'--model_id', type=str, default=None,
180245
help="An account model UUID (required for accounts with multiple models")
181-
def stake_decrease(nmr, model_id):
246+
@tournament_option
247+
def stake_decrease(nmr, model_id, tournament=DEFAULT_TOURNAMENT):
182248
"""Decrease your stake by `value` NMR."""
183-
click.echo(napi.stake_decrease(nmr, model_id))
249+
api = _get_api(tournament)
250+
click.echo(api.stake_decrease(nmr, model_id))
184251

185252

186253
@cli.command()
187254
@click.argument("nmr")
188255
@click.option(
189256
'--model_id', type=str, default=None,
190257
help="An account model UUID (required for accounts with multiple models")
191-
def stake_increase(nmr, model_id):
258+
@tournament_option
259+
def stake_increase(nmr, model_id, tournament=DEFAULT_TOURNAMENT):
192260
"""Increase your stake by `value` NMR."""
193-
click.echo(napi.stake_increase(nmr, model_id))
261+
api = _get_api(tournament)
262+
click.echo(api.stake_increase(nmr, model_id))
194263

195264

196265
@cli.command()

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Ensure tests import the local numerapi package."""
2+
3+
from __future__ import annotations
4+
5+
import sys
6+
from pathlib import Path
7+
8+
ROOT = Path(__file__).resolve().parents[1]
9+
root_str = str(ROOT)
10+
if root_str not in sys.path:
11+
sys.path.insert(0, root_str)
12+

0 commit comments

Comments
 (0)