kabukit 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kabukit-0.1.0/PKG-INFO
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: kabukit
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: Add your description here
|
5
|
+
Author: Daizu
|
6
|
+
Author-email: Daizu <daizutabi@gmail.com>
|
7
|
+
Requires-Dist: httpx
|
8
|
+
Requires-Dist: platformdirs
|
9
|
+
Requires-Dist: polars>=1.32.3
|
10
|
+
Requires-Dist: python-dotenv
|
11
|
+
Requires-Dist: typer
|
12
|
+
Requires-Python: >=3.13
|
13
|
+
Description-Content-Type: text/markdown
|
14
|
+
|
15
|
+
# kabukit
|
16
|
+
|
17
|
+
A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs.
|
18
|
+
|
19
|
+
## Gemini CLI
|
20
|
+
|
21
|
+
https://github.com/google-gemini/gemini-cli/issues/6297
|
22
|
+
|
23
|
+
```bash
|
24
|
+
echo $GEMINI_CLI_IDE_SERVER_PORT
|
25
|
+
sudo sh -c 'echo "127.0.0.1 host.docker.internal" >> /etc/hosts'
|
26
|
+
```
|
27
|
+
|
28
|
+
## References
|
29
|
+
|
30
|
+
https://jpx.gitbook.io/j-quants-ja/api-reference
|
31
|
+
https://japanexchangegroup.github.io/J-Quants-Tutorial/
|
32
|
+
https://www.jpx.co.jp/corporate/news/news-releases/0010/20210813-01.html
|
33
|
+
https://disclosure2dl.edinet-fsa.go.jp/guide/static/disclosure/WZEK0110.html
|
kabukit-0.1.0/README.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# kabukit
|
2
|
+
|
3
|
+
A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs.
|
4
|
+
|
5
|
+
## Gemini CLI
|
6
|
+
|
7
|
+
https://github.com/google-gemini/gemini-cli/issues/6297
|
8
|
+
|
9
|
+
```bash
|
10
|
+
echo $GEMINI_CLI_IDE_SERVER_PORT
|
11
|
+
sudo sh -c 'echo "127.0.0.1 host.docker.internal" >> /etc/hosts'
|
12
|
+
```
|
13
|
+
|
14
|
+
## References
|
15
|
+
|
16
|
+
https://jpx.gitbook.io/j-quants-ja/api-reference
|
17
|
+
https://japanexchangegroup.github.io/J-Quants-Tutorial/
|
18
|
+
https://www.jpx.co.jp/corporate/news/news-releases/0010/20210813-01.html
|
19
|
+
https://disclosure2dl.edinet-fsa.go.jp/guide/static/disclosure/WZEK0110.html
|
@@ -0,0 +1,77 @@
|
|
1
|
+
[build-system]
|
2
|
+
requires = ["uv_build"]
|
3
|
+
build-backend = "uv_build"
|
4
|
+
|
5
|
+
[project]
|
6
|
+
name = "kabukit"
|
7
|
+
version = "0.1.0"
|
8
|
+
description = "Add your description here"
|
9
|
+
readme = "README.md"
|
10
|
+
authors = [{ name = "Daizu", email = "daizutabi@gmail.com" }]
|
11
|
+
requires-python = ">=3.13"
|
12
|
+
dependencies = [
|
13
|
+
"httpx",
|
14
|
+
"platformdirs",
|
15
|
+
"polars>=1.32.3",
|
16
|
+
"python-dotenv",
|
17
|
+
"typer",
|
18
|
+
]
|
19
|
+
|
20
|
+
[project.scripts]
|
21
|
+
kabu = "kabukit.cli:app"
|
22
|
+
|
23
|
+
[dependency-groups]
|
24
|
+
dev = [
|
25
|
+
"matplotlib>=3.6",
|
26
|
+
"polars>=1",
|
27
|
+
"pytest-clarity>=1",
|
28
|
+
"pytest-cov>=6",
|
29
|
+
"pytest-httpx",
|
30
|
+
"pytest-mock",
|
31
|
+
"pytest-randomly>=3.16",
|
32
|
+
"pytest-xdist>=3.6",
|
33
|
+
]
|
34
|
+
docs = ["mkapi>=4.4", "mkdocs-material"]
|
35
|
+
|
36
|
+
[tool.pytest.ini_options]
|
37
|
+
addopts = ["--cov=kabukit", "--cov-report=lcov:lcov.info", "--doctest-modules"]
|
38
|
+
|
39
|
+
[tool.coverage.report]
|
40
|
+
exclude_lines = ["no cov", "raise NotImplementedError", "if TYPE_CHECKING:"]
|
41
|
+
skip_covered = true
|
42
|
+
|
43
|
+
[tool.ruff]
|
44
|
+
line-length = 88
|
45
|
+
target-version = "py311"
|
46
|
+
|
47
|
+
[tool.ruff.lint]
|
48
|
+
select = ["ALL"]
|
49
|
+
unfixable = ["F401"]
|
50
|
+
ignore = [
|
51
|
+
"A002",
|
52
|
+
"ANN401",
|
53
|
+
"D",
|
54
|
+
"FBT001",
|
55
|
+
"N802",
|
56
|
+
"PGH003",
|
57
|
+
"PD901",
|
58
|
+
"PLR0913",
|
59
|
+
"PLC0415",
|
60
|
+
"PLR2004",
|
61
|
+
"S603",
|
62
|
+
"SIM102",
|
63
|
+
"TRY003",
|
64
|
+
]
|
65
|
+
|
66
|
+
[tool.ruff.lint.per-file-ignores]
|
67
|
+
"tests/*" = ["ANN", "FBT", "S101"]
|
68
|
+
|
69
|
+
[tool.basedpyright]
|
70
|
+
include = ["src", "tests"]
|
71
|
+
reportAny = false
|
72
|
+
reportExplicitAny = false
|
73
|
+
reportImplicitOverride = false
|
74
|
+
reportImportCycles = false
|
75
|
+
reportIncompatibleVariableOverride = false
|
76
|
+
reportUnusedCallResult = false
|
77
|
+
reportUnusedImport = false
|
@@ -0,0 +1,40 @@
|
|
1
|
+
"""kabukit CLI."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Annotated
|
6
|
+
|
7
|
+
import typer
|
8
|
+
from httpx import HTTPStatusError
|
9
|
+
from typer import Argument, Exit, Option, Typer
|
10
|
+
|
11
|
+
from .jquants.client import JQuantsClient
|
12
|
+
|
13
|
+
app = Typer(add_completion=False)
|
14
|
+
|
15
|
+
|
16
|
+
@app.command()
|
17
|
+
def auth(
|
18
|
+
mailaddress: Annotated[str, Argument(help="J-Quants mail address.")],
|
19
|
+
password: Annotated[str, Option(prompt=True, hide_input=True)],
|
20
|
+
) -> None:
|
21
|
+
"""Authenticate and save/refresh tokens."""
|
22
|
+
client = JQuantsClient()
|
23
|
+
|
24
|
+
try:
|
25
|
+
client.auth(mailaddress, password)
|
26
|
+
except HTTPStatusError as e:
|
27
|
+
typer.echo(f"Authentication failed: {e}")
|
28
|
+
raise Exit(1) from None
|
29
|
+
|
30
|
+
client = JQuantsClient()
|
31
|
+
typer.echo(f"refreshToken: {client.refresh_token[:30]}...")
|
32
|
+
typer.echo(f"idToken: {client.id_token[:30]}...")
|
33
|
+
|
34
|
+
|
35
|
+
@app.command()
|
36
|
+
def version() -> None:
|
37
|
+
"""Show kabukit version."""
|
38
|
+
from importlib.metadata import version
|
39
|
+
|
40
|
+
typer.echo(f"kabukit version: {version('kabukit')}")
|
File without changes
|
@@ -0,0 +1,324 @@
|
|
1
|
+
"""This module provides a client for the J-Quants API.
|
2
|
+
|
3
|
+
It handles authentication and provides methods to interact with
|
4
|
+
the API endpoints.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
import datetime
|
10
|
+
import os
|
11
|
+
from enum import StrEnum
|
12
|
+
from functools import cached_property
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import TYPE_CHECKING
|
15
|
+
|
16
|
+
import polars as pl
|
17
|
+
from dotenv import load_dotenv, set_key
|
18
|
+
from httpx import Client
|
19
|
+
from platformdirs import user_config_dir
|
20
|
+
from polars import DataFrame
|
21
|
+
|
22
|
+
if TYPE_CHECKING:
|
23
|
+
from collections.abc import Iterator
|
24
|
+
from typing import Any
|
25
|
+
|
26
|
+
from httpx import HTTPStatusError # noqa: F401
|
27
|
+
from httpx._types import QueryParamTypes
|
28
|
+
|
29
|
+
API_VERSION = "v1"
|
30
|
+
|
31
|
+
|
32
|
+
class AuthenticationError(Exception):
|
33
|
+
"""Custom exception for authentication failures."""
|
34
|
+
|
35
|
+
|
36
|
+
class AuthKey(StrEnum):
|
37
|
+
"""Environment variable keys for J-Quants authentication."""
|
38
|
+
|
39
|
+
REFRESH_TOKEN = "JQUANTS_REFRESH_TOKEN" # noqa: S105
|
40
|
+
ID_TOKEN = "JQUANTS_ID_TOKEN" # noqa: S105
|
41
|
+
|
42
|
+
|
43
|
+
class JQuantsClient:
|
44
|
+
"""A client for interacting with the J-Quants API.
|
45
|
+
|
46
|
+
This client manages API authentication tokens (refresh and ID)
|
47
|
+
and provides methods to access various J-Quants API
|
48
|
+
endpoints. Tokens are loaded from and saved to a file in the
|
49
|
+
user's standard config directory.
|
50
|
+
|
51
|
+
Attributes:
|
52
|
+
client: An httpx.Client instance for making API requests.
|
53
|
+
refresh_token: The refresh token for authentication.
|
54
|
+
id_token: The ID token for API requests.
|
55
|
+
"""
|
56
|
+
|
57
|
+
client: Client
|
58
|
+
refresh_token: str | None
|
59
|
+
id_token: str | None
|
60
|
+
|
61
|
+
def __init__(self) -> None:
|
62
|
+
"""Initializes the JQuantsClient.
|
63
|
+
|
64
|
+
It sets up the httpx client, determines the config path,
|
65
|
+
loads authentication tokens, and sets the auth header if an
|
66
|
+
ID token is present.
|
67
|
+
"""
|
68
|
+
self.client = Client(base_url=f"https://api.jquants.com/{API_VERSION}")
|
69
|
+
self._setup_config_path()
|
70
|
+
self._load_tokens()
|
71
|
+
self.set_header()
|
72
|
+
|
73
|
+
@cached_property
|
74
|
+
def dotenv_path(self) -> Path:
|
75
|
+
"""Returns the path to the .env file in the user config directory."""
|
76
|
+
config_dir = Path(user_config_dir("kabukit"))
|
77
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
78
|
+
return config_dir / ".env"
|
79
|
+
|
80
|
+
def _setup_config_path(self) -> None:
|
81
|
+
"""Determines the config path and creates the directory."""
|
82
|
+
# Accessing dotenv_path property will create the directory if it doesn't exist
|
83
|
+
_ = self.dotenv_path
|
84
|
+
|
85
|
+
def _load_tokens(self) -> None:
|
86
|
+
"""Loads tokens from the .env file."""
|
87
|
+
load_dotenv(self.dotenv_path)
|
88
|
+
self.refresh_token = os.environ.get(AuthKey.REFRESH_TOKEN)
|
89
|
+
self.id_token = os.environ.get(AuthKey.ID_TOKEN)
|
90
|
+
|
91
|
+
def set_header(self) -> None:
|
92
|
+
"""Sets the Authorization header if an ID token is available."""
|
93
|
+
if self.id_token:
|
94
|
+
self.client.headers["Authorization"] = f"Bearer {self.id_token}"
|
95
|
+
# Clear header if no ID token is available
|
96
|
+
elif "Authorization" in self.client.headers:
|
97
|
+
del self.client.headers["Authorization"]
|
98
|
+
|
99
|
+
def auth(self, mailaddress: str, password: str) -> None:
|
100
|
+
"""Authenticates, saves tokens, and sets the auth header.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
mailaddress: The user's email address.
|
104
|
+
password: The user's password.
|
105
|
+
|
106
|
+
Raises:
|
107
|
+
HTTPStatusError: If any API request fails.
|
108
|
+
"""
|
109
|
+
self.refresh_token = self.get_refresh_token(mailaddress, password)
|
110
|
+
self.id_token = self.get_id_token(self.refresh_token)
|
111
|
+
set_key(self.dotenv_path, AuthKey.REFRESH_TOKEN, self.refresh_token)
|
112
|
+
set_key(self.dotenv_path, AuthKey.ID_TOKEN, self.id_token)
|
113
|
+
self.set_header()
|
114
|
+
|
115
|
+
def post(self, url: str, json: Any | None = None) -> Any:
|
116
|
+
"""Sends a POST request to the specified URL.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
url: The URL path for the POST request.
|
120
|
+
json: The JSON payload for the request body.
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
The JSON response from the API.
|
124
|
+
|
125
|
+
Raises:
|
126
|
+
AuthenticationError: If no ID token is available.
|
127
|
+
HTTPStatusError: If the API request fails.
|
128
|
+
"""
|
129
|
+
if not self.id_token:
|
130
|
+
msg = "ID token is not available. Please authenticate first."
|
131
|
+
raise AuthenticationError(msg)
|
132
|
+
|
133
|
+
resp = self.client.post(url, json=json)
|
134
|
+
resp.raise_for_status()
|
135
|
+
return resp.json()
|
136
|
+
|
137
|
+
def get_refresh_token(self, mailaddress: str, password: str) -> str:
|
138
|
+
"""Gets a new refresh token from the API.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
mailaddress: The user's email address.
|
142
|
+
password: The user's password.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
The new refresh token.
|
146
|
+
|
147
|
+
Raises:
|
148
|
+
httpx.HTTPStatusError: If the API request fails.
|
149
|
+
"""
|
150
|
+
json_data = {"mailaddress": mailaddress, "password": password}
|
151
|
+
return self.post("/token/auth_user", json=json_data)["refreshToken"]
|
152
|
+
|
153
|
+
def get_id_token(self, refresh_token: str) -> str:
|
154
|
+
"""Gets a new ID token from the API.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
refresh_token: The refresh token to use.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
The new ID token.
|
161
|
+
|
162
|
+
Raises:
|
163
|
+
HTTPStatusError: If the API request fails.
|
164
|
+
"""
|
165
|
+
url = f"/token/auth_refresh?refreshtoken={refresh_token}"
|
166
|
+
return self.post(url)["idToken"]
|
167
|
+
|
168
|
+
def get(self, url: str, params: QueryParamTypes | None = None) -> Any:
|
169
|
+
"""Sends a GET request to the specified URL.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
url: The URL path for the GET request.
|
173
|
+
params: The query parameters for the request.
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
The JSON response from the API.
|
177
|
+
|
178
|
+
Raises:
|
179
|
+
AuthenticationError: If no ID token is available.
|
180
|
+
HTTPStatusError: If the API request fails.
|
181
|
+
"""
|
182
|
+
if not self.id_token:
|
183
|
+
msg = "ID token is not available. Please authenticate first."
|
184
|
+
raise AuthenticationError(msg)
|
185
|
+
|
186
|
+
resp = self.client.get(url, params=params)
|
187
|
+
resp.raise_for_status()
|
188
|
+
return resp.json()
|
189
|
+
|
190
|
+
def get_listed_info(
|
191
|
+
self,
|
192
|
+
code: str | None = None,
|
193
|
+
date: str | datetime.date | None = None,
|
194
|
+
) -> DataFrame:
|
195
|
+
"""Gets listed info (e.g., stock details) from the API.
|
196
|
+
|
197
|
+
Args:
|
198
|
+
code: Optional. The stock code to filter by.
|
199
|
+
date: Optional. The date to filter by (YYYY-MM-DD format
|
200
|
+
or datetime.date object).
|
201
|
+
|
202
|
+
Returns:
|
203
|
+
A Polars DataFrame containing the listed info.
|
204
|
+
|
205
|
+
Raises:
|
206
|
+
AuthenticationError: If no ID token is available.
|
207
|
+
HTTPStatusError: If the API request fails.
|
208
|
+
"""
|
209
|
+
params = params_code_date(code, date)
|
210
|
+
url = "/listed/info"
|
211
|
+
data = self.get(url, params)
|
212
|
+
df = DataFrame(data["info"])
|
213
|
+
return df.with_columns(pl.col("Date").str.to_date())
|
214
|
+
|
215
|
+
def iter_pagaes(
|
216
|
+
self,
|
217
|
+
url: str,
|
218
|
+
params: dict[str, Any] | None,
|
219
|
+
name: str,
|
220
|
+
) -> Iterator[DataFrame]:
|
221
|
+
"""Iterates through paginated API responses.
|
222
|
+
|
223
|
+
Args:
|
224
|
+
url: The base URL for the API endpoint.
|
225
|
+
params: Optional. Dictionary of query parameters.
|
226
|
+
name: The key in the JSON response containing the list of items.
|
227
|
+
|
228
|
+
Yields:
|
229
|
+
A Polars DataFrame for each page of data.
|
230
|
+
|
231
|
+
Raises:
|
232
|
+
AuthenticationError: If no ID token is available.
|
233
|
+
HTTPStatusError: If the API request fails.
|
234
|
+
"""
|
235
|
+
params = params or {}
|
236
|
+
|
237
|
+
while True:
|
238
|
+
data = self.get(url, params)
|
239
|
+
yield DataFrame(data[name])
|
240
|
+
if "pagination_key" in data:
|
241
|
+
params["pagination_key"] = data["pagination_key"]
|
242
|
+
else:
|
243
|
+
break
|
244
|
+
|
245
|
+
def get_prices(
|
246
|
+
self,
|
247
|
+
code: str | None = None,
|
248
|
+
date: str | datetime.date | None = None,
|
249
|
+
from_: str | datetime.date | None = None,
|
250
|
+
to: str | datetime.date | None = None,
|
251
|
+
) -> DataFrame:
|
252
|
+
"""Gets daily stock prices from the API.
|
253
|
+
|
254
|
+
Args:
|
255
|
+
code: Optional. The stock code to filter by.
|
256
|
+
date: Optional. The specific date for which to retrieve prices.
|
257
|
+
Cannot be used with `from_` or `to`.
|
258
|
+
from_: Optional. The start date for a price range.
|
259
|
+
Requires `to` if `date` is not specified.
|
260
|
+
to: Optional. The end date for a price range.
|
261
|
+
Requires `from_` if `date` is not specified.
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
A Polars DataFrame containing daily stock prices.
|
265
|
+
|
266
|
+
Raises:
|
267
|
+
ValueError: If both `date` and `from_`/`to` are specified.
|
268
|
+
AuthenticationError: If no ID token is available.
|
269
|
+
HTTPStatusError: If the API request fails.
|
270
|
+
"""
|
271
|
+
params = params_code_date(code, date)
|
272
|
+
|
273
|
+
if date and (from_ or to):
|
274
|
+
msg = "Cannot specify both date and from/to parameters."
|
275
|
+
raise ValueError(msg)
|
276
|
+
|
277
|
+
if not date and from_:
|
278
|
+
params["from"] = date_to_str(from_)
|
279
|
+
if not date and to:
|
280
|
+
params["to"] = date_to_str(to)
|
281
|
+
|
282
|
+
url = "/prices/daily_quotes"
|
283
|
+
name = "daily_quotes"
|
284
|
+
|
285
|
+
df = pl.concat(self.iter_pagaes(url, params, name))
|
286
|
+
if df.is_empty():
|
287
|
+
return df
|
288
|
+
|
289
|
+
return df.with_columns(pl.col("Date").str.to_date())
|
290
|
+
|
291
|
+
|
292
|
+
def params_code_date(
|
293
|
+
code: str | None,
|
294
|
+
date: str | datetime.date | None,
|
295
|
+
) -> dict[str, str]:
|
296
|
+
"""Constructs a dictionary of parameters for code and date filtering.
|
297
|
+
|
298
|
+
Args:
|
299
|
+
code: Optional. The stock code.
|
300
|
+
date: Optional. The date (string or datetime.date object).
|
301
|
+
|
302
|
+
Returns:
|
303
|
+
A dictionary containing 'code' and/or 'date' parameters.
|
304
|
+
"""
|
305
|
+
params: dict[str, str] = {}
|
306
|
+
if code:
|
307
|
+
params["code"] = code
|
308
|
+
if date:
|
309
|
+
params["date"] = date_to_str(date)
|
310
|
+
return params
|
311
|
+
|
312
|
+
|
313
|
+
def date_to_str(date: str | datetime.date) -> str:
|
314
|
+
"""Converts a date object or string to a YYYY-MM-DD string.
|
315
|
+
|
316
|
+
Args:
|
317
|
+
date: The date to convert (string or datetime.date object).
|
318
|
+
|
319
|
+
Returns:
|
320
|
+
The date as a YYYY-MM-DD string.
|
321
|
+
"""
|
322
|
+
if isinstance(date, datetime.date):
|
323
|
+
return date.strftime("%Y-%m-%d")
|
324
|
+
return date
|