kabukit 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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/jquants/client.py CHANGED
@@ -1,324 +1,362 @@
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
1
  from __future__ import annotations
8
2
 
9
3
  import datetime
10
4
  import os
11
5
  from enum import StrEnum
12
- from functools import cached_property
13
- from pathlib import Path
14
6
  from typing import TYPE_CHECKING
15
7
 
16
8
  import polars as pl
17
- from dotenv import load_dotenv, set_key
18
- from httpx import Client
19
- from platformdirs import user_config_dir
9
+ from httpx import AsyncClient
20
10
  from polars import DataFrame
21
11
 
12
+ from kabukit.config import load_dotenv, set_key
13
+ from kabukit.params import get_params
14
+
15
+ from . import statements
16
+
22
17
  if TYPE_CHECKING:
23
- from collections.abc import Iterator
24
- from typing import Any
18
+ from collections.abc import AsyncIterator
19
+ from typing import Any, Self
25
20
 
26
21
  from httpx import HTTPStatusError # noqa: F401
27
22
  from httpx._types import QueryParamTypes
28
23
 
29
24
  API_VERSION = "v1"
30
-
31
-
32
- class AuthenticationError(Exception):
33
- """Custom exception for authentication failures."""
25
+ BASE_URL = f"https://api.jquants.com/{API_VERSION}"
34
26
 
35
27
 
36
28
  class AuthKey(StrEnum):
37
- """Environment variable keys for J-Quants authentication."""
29
+ """J-Quants認証のための環境変数キー。"""
38
30
 
39
31
  REFRESH_TOKEN = "JQUANTS_REFRESH_TOKEN" # noqa: S105
40
32
  ID_TOKEN = "JQUANTS_ID_TOKEN" # noqa: S105
41
33
 
42
34
 
43
35
  class JQuantsClient:
44
- """A client for interacting with the J-Quants API.
36
+ """J-Quants APIと対話するためのクライアント。
45
37
 
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.
38
+ API認証トークン(リフレッシュトークンおよびIDトークン)を管理し、
39
+ 各種J-Quants APIエンドポイントへアクセスするメソッドを提供する。
40
+ トークンは設定ファイルから読み込まれ、またファイルに保存される。
50
41
 
51
42
  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.
43
+ client: APIリクエストを行うための `AsyncClient` インスタンス。
55
44
  """
56
45
 
57
- client: Client
58
- refresh_token: str | None
59
- id_token: str | None
46
+ client: AsyncClient
60
47
 
61
- def __init__(self) -> None:
62
- """Initializes the JQuantsClient.
48
+ def __init__(self, id_token: str | None = None) -> None:
49
+ self.client = AsyncClient(base_url=BASE_URL)
50
+ self.set_id_token(id_token)
63
51
 
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.
52
+ def set_id_token(self, id_token: str | None = None) -> None:
53
+ """IDトークンをヘッダーに設定する。
54
+
55
+ Args:
56
+ id_token (str | None, optional): 設定するIDトークン。
57
+ Noneの場合、環境変数から読み込む。
67
58
  """
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.
59
+
60
+ if id_token is None:
61
+ load_dotenv()
62
+ id_token = os.environ.get(AuthKey.ID_TOKEN)
63
+
64
+ if id_token:
65
+ self.client.headers["Authorization"] = f"Bearer {id_token}"
66
+
67
+ async def aclose(self) -> None:
68
+ """HTTPクライアントを閉じる。"""
69
+ await self.client.aclose()
70
+
71
+ async def __aenter__(self) -> Self:
72
+ return self
73
+
74
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] # noqa: ANN001
75
+ await self.aclose()
76
+
77
+ async def auth(
78
+ self,
79
+ mailaddress: str,
80
+ password: str,
81
+ *,
82
+ save: bool = False,
83
+ ) -> Self:
84
+ """認証を行い、トークンを保存する。
101
85
 
102
86
  Args:
103
- mailaddress: The user's email address.
104
- password: The user's password.
87
+ mailaddress (str): J-Quantsに登録したメールアドレス。
88
+ password (str): J-Quantsのパスワード。
89
+ save (bool, optional): トークンを環境変数に保存するかどうか。
105
90
 
106
91
  Raises:
107
- HTTPStatusError: If any API request fails.
92
+ HTTPStatusError: APIリクエストが失敗した場合。
108
93
  """
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()
94
+ refresh_token = await self.get_refresh_token(mailaddress, password)
95
+ id_token = await self.get_id_token(refresh_token)
96
+
97
+ if save:
98
+ set_key(AuthKey.REFRESH_TOKEN, refresh_token)
99
+ set_key(AuthKey.ID_TOKEN, id_token)
114
100
 
115
- def post(self, url: str, json: Any | None = None) -> Any:
116
- """Sends a POST request to the specified URL.
101
+ self.set_id_token(id_token)
102
+ return self
103
+
104
+ async def post(self, url: str, json: Any | None = None) -> Any:
105
+ """指定されたURLにPOSTリクエストを送信する。
117
106
 
118
107
  Args:
119
- url: The URL path for the POST request.
120
- json: The JSON payload for the request body.
108
+ url: POSTリクエストのURLパス。
109
+ json: リクエストボディのJSONペイロード。
121
110
 
122
111
  Returns:
123
- The JSON response from the API.
112
+ APIからのJSONレスポンス。
124
113
 
125
114
  Raises:
126
- AuthenticationError: If no ID token is available.
127
- HTTPStatusError: If the API request fails.
115
+ HTTPStatusError: APIリクエストが失敗した場合。
128
116
  """
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)
117
+ resp = await self.client.post(url, json=json)
134
118
  resp.raise_for_status()
135
119
  return resp.json()
136
120
 
137
- def get_refresh_token(self, mailaddress: str, password: str) -> str:
138
- """Gets a new refresh token from the API.
121
+ async def get_refresh_token(self, mailaddress: str, password: str) -> str:
122
+ """APIから新しいリフレッシュトークンを取得する。
139
123
 
140
124
  Args:
141
- mailaddress: The user's email address.
142
- password: The user's password.
125
+ mailaddress (str): ユーザーのメールアドレス。
126
+ password (str): ユーザーのパスワード。
143
127
 
144
128
  Returns:
145
- The new refresh token.
129
+ 新しいリフレッシュトークン。
146
130
 
147
131
  Raises:
148
- httpx.HTTPStatusError: If the API request fails.
132
+ HTTPStatusError: APIリクエストが失敗した場合。
149
133
  """
150
134
  json_data = {"mailaddress": mailaddress, "password": password}
151
- return self.post("/token/auth_user", json=json_data)["refreshToken"]
135
+ data = await self.post("/token/auth_user", json=json_data)
136
+ return data["refreshToken"]
152
137
 
153
- def get_id_token(self, refresh_token: str) -> str:
154
- """Gets a new ID token from the API.
138
+ async def get_id_token(self, refresh_token: str) -> str:
139
+ """APIから新しいIDトークンを取得する。
155
140
 
156
141
  Args:
157
- refresh_token: The refresh token to use.
142
+ refresh_token (str): 使用するリフレッシュトークン。
158
143
 
159
144
  Returns:
160
- The new ID token.
145
+ 新しいIDトークン。
161
146
 
162
147
  Raises:
163
- HTTPStatusError: If the API request fails.
148
+ HTTPStatusError: APIリクエストが失敗した場合。
164
149
  """
165
150
  url = f"/token/auth_refresh?refreshtoken={refresh_token}"
166
- return self.post(url)["idToken"]
151
+ data = await self.post(url)
152
+ return data["idToken"]
167
153
 
168
- def get(self, url: str, params: QueryParamTypes | None = None) -> Any:
169
- """Sends a GET request to the specified URL.
154
+ async def get(self, url: str, params: QueryParamTypes | None = None) -> Any:
155
+ """指定されたURLにGETリクエストを送信する。
170
156
 
171
157
  Args:
172
- url: The URL path for the GET request.
173
- params: The query parameters for the request.
158
+ url (str): GETリクエストのURLパス。
159
+ params (QueryParamTypes | None, optional): リクエストのクエリパラメータ。
174
160
 
175
161
  Returns:
176
- The JSON response from the API.
162
+ APIからのJSONレスポンス。
177
163
 
178
164
  Raises:
179
- AuthenticationError: If no ID token is available.
180
- HTTPStatusError: If the API request fails.
165
+ HTTPStatusError: APIリクエストが失敗した場合。
181
166
  """
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)
167
+ resp = await self.client.get(url, params=params)
187
168
  resp.raise_for_status()
188
169
  return resp.json()
189
170
 
190
- def get_listed_info(
171
+ async def get_info(
191
172
  self,
192
173
  code: str | None = None,
193
174
  date: str | datetime.date | None = None,
194
175
  ) -> DataFrame:
195
- """Gets listed info (e.g., stock details) from the API.
176
+ """銘柄情報を取得する。
196
177
 
197
178
  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).
179
+ code (str | None, optional): 情報を取得する銘柄のコード。
180
+ date (str | datetime.date | None, optional): 情報を取得する日付。
201
181
 
202
182
  Returns:
203
- A Polars DataFrame containing the listed info.
183
+ 銘柄情報を含むPolars DataFrame
204
184
 
205
185
  Raises:
206
- AuthenticationError: If no ID token is available.
207
- HTTPStatusError: If the API request fails.
186
+ HTTPStatusError: APIリクエストが失敗した場合。
208
187
  """
209
- params = params_code_date(code, date)
188
+ params = get_params(code=code, date=date)
210
189
  url = "/listed/info"
211
- data = self.get(url, params)
190
+ data = await self.get(url, params)
212
191
  df = DataFrame(data["info"])
213
- return df.with_columns(pl.col("Date").str.to_date())
214
192
 
215
- def iter_pagaes(
193
+ return df.with_columns(
194
+ pl.col("Date").str.to_date("%Y-%m-%d"),
195
+ pl.col("^.*CodeName$", "ScaleCategory").cast(pl.Categorical),
196
+ ).drop("^.+Code$", "CompanyNameEnglish")
197
+
198
+ async def iter_pages(
216
199
  self,
217
200
  url: str,
218
201
  params: dict[str, Any] | None,
219
202
  name: str,
220
- ) -> Iterator[DataFrame]:
221
- """Iterates through paginated API responses.
203
+ ) -> AsyncIterator[DataFrame]:
204
+ """ページ分割されたAPIレスポンスを反復処理する。
222
205
 
223
206
  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.
207
+ url (str): APIエンドポイントのベースURL
208
+ params (dict[str, Any]): クエリパラメータの辞書。
209
+ name (str): アイテムのリストを含むJSONレスポンスのキー。
227
210
 
228
211
  Yields:
229
- A Polars DataFrame for each page of data.
212
+ データの各ページに対応するPolars DataFrame
230
213
 
231
214
  Raises:
232
- AuthenticationError: If no ID token is available.
233
- HTTPStatusError: If the API request fails.
215
+ HTTPStatusError: APIリクエストが失敗した場合。
234
216
  """
235
217
  params = params or {}
236
218
 
237
219
  while True:
238
- data = self.get(url, params)
220
+ data = await self.get(url, params)
239
221
  yield DataFrame(data[name])
240
222
  if "pagination_key" in data:
241
223
  params["pagination_key"] = data["pagination_key"]
242
224
  else:
243
225
  break
244
226
 
245
- def get_prices(
227
+ async def get_prices(
246
228
  self,
247
229
  code: str | None = None,
248
230
  date: str | datetime.date | None = None,
249
231
  from_: str | datetime.date | None = None,
250
232
  to: str | datetime.date | None = None,
251
233
  ) -> DataFrame:
252
- """Gets daily stock prices from the API.
234
+ """日々の株価四本値を取得する。
253
235
 
254
236
  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.
237
+ code: 株価を取得する銘柄のコード。
238
+ date: 株価を取得する特定の日付。`from_`または`to`とは併用不可。
239
+ from_: 取得期間の開始日。`date`とは併用不可。
240
+ to: 取得期間の終了日。`date`とは併用不可。
262
241
 
263
242
  Returns:
264
- A Polars DataFrame containing daily stock prices.
243
+ 日々の株価四本値を含むPolars DataFrame
265
244
 
266
245
  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.
246
+ ValueError: `date`と`from_`/`to`の両方が指定された場合。
247
+ HTTPStatusError: APIリクエストが失敗した場合。
270
248
  """
271
- params = params_code_date(code, date)
249
+ if not date and not code:
250
+ return await self.get_latest_available_prices()
272
251
 
273
252
  if date and (from_ or to):
274
253
  msg = "Cannot specify both date and from/to parameters."
275
254
  raise ValueError(msg)
276
255
 
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)
256
+ params = get_params(code=code, date=date, from_=from_, to=to)
281
257
 
282
258
  url = "/prices/daily_quotes"
283
259
  name = "daily_quotes"
284
260
 
285
- df = pl.concat(self.iter_pagaes(url, params, name))
261
+ dfs = [df async for df in self.iter_pages(url, params, name)]
262
+ df = pl.concat(dfs)
263
+
286
264
  if df.is_empty():
287
265
  return df
288
266
 
289
- return df.with_columns(pl.col("Date").str.to_date())
267
+ return df.with_columns(
268
+ pl.col("Date").str.to_date("%Y-%m-%d"),
269
+ pl.col("^.*Limit$").cast(pl.Int8).cast(pl.Boolean),
270
+ )
290
271
 
272
+ async def get_latest_available_prices(self) -> DataFrame:
273
+ """直近利用可能な日付の株価を取得する。"""
274
+ today = datetime.date.today() # noqa: DTZ011
291
275
 
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.
276
+ for days in range(30):
277
+ date = today - datetime.timedelta(days)
278
+ df = await self.get_prices(date=date)
297
279
 
298
- Args:
299
- code: Optional. The stock code.
300
- date: Optional. The date (string or datetime.date object).
280
+ if not df.is_empty():
281
+ return df
301
282
 
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
283
+ return DataFrame()
284
+
285
+ async def get_statements(
286
+ self,
287
+ code: str | None = None,
288
+ date: str | datetime.date | None = None,
289
+ ) -> DataFrame:
290
+ """財務情報を取得する。
311
291
 
292
+ Args:
293
+ code: 財務情報を取得する銘柄のコード。
294
+ date: 財務情報を取得する日付。
312
295
 
313
- def date_to_str(date: str | datetime.date) -> str:
314
- """Converts a date object or string to a YYYY-MM-DD string.
296
+ Returns:
297
+ 財務情報を含むPolars DataFrame。
315
298
 
316
- Args:
317
- date: The date to convert (string or datetime.date object).
299
+ Raises:
300
+ HTTPStatusError: APIリクエストが失敗した場合。
301
+ """
302
+ params = get_params(code=code, date=date)
303
+ url = "/fins/statements"
304
+ name = "statements"
318
305
 
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
306
+ dfs = [df async for df in self.iter_pages(url, params, name)]
307
+ df = pl.concat(dfs)
308
+
309
+ if df.is_empty():
310
+ return df
311
+
312
+ return statements.clean(df)
313
+
314
+ async def get_announcement(self) -> DataFrame:
315
+ """翌日発表予定の決算情報を取得する。
316
+
317
+ Returns:
318
+ 開示情報を含むPolars DataFrame。
319
+
320
+ Raises:
321
+ HTTPStatusError: APIリクエストが失敗した場合。
322
+ """
323
+ url = "fins/announcement"
324
+ name = "announcement"
325
+
326
+ dfs = [df async for df in self.iter_pages(url, {}, name)]
327
+ df = pl.concat(dfs)
328
+ if df.is_empty():
329
+ return df
330
+
331
+ return df.with_columns(pl.col("Date").str.to_date("%Y-%m-%d", strict=False))
332
+
333
+ async def get_trades_spec(
334
+ self,
335
+ section: str | None = None,
336
+ from_: str | datetime.date | None = None,
337
+ to: str | datetime.date | None = None,
338
+ ) -> DataFrame:
339
+ """投資部門別の情報を取得する。
340
+
341
+ Args:
342
+ section: 絞り込み対象のセクション。
343
+ from_: 取得期間の開始日。
344
+ to: 取得期間の終了日。
345
+
346
+ Returns:
347
+ 投資部門別の情報を含むPolars DataFrame。
348
+
349
+ Raises:
350
+ HTTPStatusError: APIリクエストが失敗した場合。
351
+ """
352
+ params = get_params(section=section, from_=from_, to=to)
353
+
354
+ url = "/markets/trades_spec"
355
+ name = "trades_spec"
356
+
357
+ dfs = [df async for df in self.iter_pages(url, params, name)]
358
+ df = pl.concat(dfs)
359
+ if df.is_empty():
360
+ return df
361
+
362
+ return df.with_columns(pl.col("^.*Date$").str.to_date("%Y-%m-%d", strict=False))
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import polars as pl
4
+
5
+ from .client import JQuantsClient
6
+
7
+
8
+ async def get_codes() -> list[str]:
9
+ """銘柄コードのリストを返す。
10
+
11
+ 市場「TOKYO PRO MARKET」と業種「その他」を除外した銘柄を対象とする。
12
+ """
13
+ async with JQuantsClient() as client:
14
+ info = await client.get_info()
15
+
16
+ return (
17
+ info.filter(
18
+ pl.col("MarketCodeName") != "TOKYO PRO MARKET",
19
+ pl.col("Sector17CodeName") != "その他",
20
+ )
21
+ .get_column("Code")
22
+ .to_list()
23
+ )
File without changes