kabukit 0.5.4__tar.gz → 0.7.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.
Files changed (43) hide show
  1. {kabukit-0.5.4 → kabukit-0.7.0}/PKG-INFO +4 -2
  2. {kabukit-0.5.4 → kabukit-0.7.0}/pyproject.toml +5 -4
  3. kabukit-0.7.0/src/kabukit/analysis/visualization/__init__.py +7 -0
  4. kabukit-0.7.0/src/kabukit/analysis/visualization/market.py +29 -0
  5. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/cli/app.py +2 -1
  6. kabukit-0.7.0/src/kabukit/cli/cache.py +61 -0
  7. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/cli/get.py +18 -3
  8. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/base.py +4 -3
  9. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/jquants/client.py +30 -1
  10. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/jquants/concurrent.py +2 -2
  11. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/jquants/info.py +7 -3
  12. kabukit-0.7.0/src/kabukit/jquants/topix.py +16 -0
  13. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/utils/concurrent.py +28 -23
  14. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/utils/config.py +5 -1
  15. {kabukit-0.5.4 → kabukit-0.7.0}/LICENSE +0 -0
  16. {kabukit-0.5.4 → kabukit-0.7.0}/README.md +0 -0
  17. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/__init__.py +0 -0
  18. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/analysis/__init__.py +0 -0
  19. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/analysis/indicators.py +0 -0
  20. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/analysis/preprocess.py +0 -0
  21. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/analysis/screener.py +0 -0
  22. /kabukit-0.5.4/src/kabukit/analysis/visualization.py → /kabukit-0.7.0/src/kabukit/analysis/visualization/prices.py +0 -0
  23. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/cli/__init__.py +0 -0
  24. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/cli/auth.py +0 -0
  25. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/__init__.py +0 -0
  26. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/client.py +0 -0
  27. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/info.py +0 -0
  28. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/list.py +0 -0
  29. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/prices.py +0 -0
  30. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/reports.py +0 -0
  31. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/core/statements.py +0 -0
  32. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/edinet/__init__.py +0 -0
  33. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/edinet/client.py +0 -0
  34. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/edinet/concurrent.py +0 -0
  35. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/edinet/doc.py +0 -0
  36. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/jquants/__init__.py +0 -0
  37. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/jquants/prices.py +0 -0
  38. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/jquants/schema.py +0 -0
  39. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/jquants/statements.py +0 -0
  40. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/py.typed +0 -0
  41. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/utils/__init__.py +0 -0
  42. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/utils/date.py +0 -0
  43. {kabukit-0.5.4 → kabukit-0.7.0}/src/kabukit/utils/params.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kabukit
3
- Version: 0.5.4
3
+ Version: 0.7.0
4
4
  Summary: A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs.
5
5
  Author: daizutabi
6
6
  Author-email: daizutabi <daizutabi@gmail.com>
@@ -29,11 +29,13 @@ Classifier: Development Status :: 4 - Beta
29
29
  Classifier: Programming Language :: Python
30
30
  Classifier: Programming Language :: Python :: 3.13
31
31
  Requires-Dist: async-typer>=0.1.10
32
- Requires-Dist: holidays>=0.81
32
+ Requires-Dist: holidays>=0.82
33
33
  Requires-Dist: httpx>=0.28.1
34
34
  Requires-Dist: platformdirs>=4.4.0
35
35
  Requires-Dist: polars>=1.34.0
36
36
  Requires-Dist: python-dotenv>=1.1.1
37
+ Requires-Dist: rich>=14.1.0
38
+ Requires-Dist: tqdm>=4.67.1
37
39
  Requires-Dist: typer>=0.19.2
38
40
  Requires-Python: >=3.13
39
41
  Project-URL: Documentation, https://daizutabi.github.io/kabukit/
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "kabukit"
7
- version = "0.5.4"
7
+ version = "0.7.0"
8
8
  description = "A Python toolkit for Japanese financial market data, supporting J-Quants and EDINET APIs."
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -17,11 +17,13 @@ classifiers = [
17
17
  requires-python = ">=3.13"
18
18
  dependencies = [
19
19
  "async-typer>=0.1.10",
20
- "holidays>=0.81",
20
+ "holidays>=0.82",
21
21
  "httpx>=0.28.1",
22
22
  "platformdirs>=4.4.0",
23
23
  "polars>=1.34.0",
24
24
  "python-dotenv>=1.1.1",
25
+ "rich>=14.1.0",
26
+ "tqdm>=4.67.1",
25
27
  "typer>=0.19.2",
26
28
  ]
27
29
 
@@ -45,8 +47,7 @@ dev = [
45
47
  "pytest-mock>=3.15.1",
46
48
  "pytest-randomly>=4.0.1",
47
49
  "pytest-xdist>=3.8.0",
48
- "rich>=14.1.0",
49
- "tqdm>=4.67.1",
50
+ "scipy",
50
51
  "vegafusion-python-embed>=1.6.9",
51
52
  "vegafusion>=2.0.3",
52
53
  "vl-convert-python>=1.8.0",
@@ -0,0 +1,7 @@
1
+ from .market import plot_topix_timeseries
2
+ from .prices import plot_prices
3
+
4
+ __all__ = [
5
+ "plot_prices",
6
+ "plot_topix_timeseries",
7
+ ]
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import altair as alt
6
+
7
+ if TYPE_CHECKING:
8
+ from polars import DataFrame
9
+
10
+ # pyright: reportUnknownMemberType=false
11
+
12
+
13
+ def plot_topix_timeseries(df: DataFrame) -> alt.Chart:
14
+ """TOPIXの時系列データを折れ線グラフでプロットする。"""
15
+ return (
16
+ alt.Chart(df, title="TOPIX 時系列チャート")
17
+ .mark_line()
18
+ .encode(
19
+ x=alt.X("Date:T", title="日付"),
20
+ y=alt.Y("Close:Q", title="終値", scale=alt.Scale(zero=False)),
21
+ tooltip=[
22
+ alt.Tooltip("Date:T", title="日付"),
23
+ alt.Tooltip("Open:Q", title="始値"),
24
+ alt.Tooltip("High:Q", title="高値"),
25
+ alt.Tooltip("Low:Q", title="安値"),
26
+ alt.Tooltip("Close:Q", title="終値"),
27
+ ],
28
+ )
29
+ )
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import typer
6
6
  from async_typer import AsyncTyper # pyright: ignore[reportMissingTypeStubs]
7
7
 
8
- from . import auth, get
8
+ from . import auth, cache, get
9
9
 
10
10
  app = AsyncTyper(
11
11
  add_completion=False,
@@ -13,6 +13,7 @@ app = AsyncTyper(
13
13
  )
14
14
  app.add_typer(auth.app, name="auth")
15
15
  app.add_typer(get.app, name="get")
16
+ app.add_typer(cache.app, name="cache")
16
17
 
17
18
 
18
19
  @app.command()
@@ -0,0 +1,61 @@
1
+ """Cache management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from typing import TYPE_CHECKING
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.tree import Tree
11
+
12
+ from kabukit.utils.config import get_cache_dir
13
+
14
+ if TYPE_CHECKING:
15
+ from pathlib import Path
16
+
17
+ app = typer.Typer(add_completion=False, help="キャッシュを管理します。")
18
+
19
+
20
+ def add_to_tree(tree: Tree, path: Path) -> None:
21
+ for p in sorted(path.iterdir()):
22
+ if p.is_dir():
23
+ branch = tree.add(p.name)
24
+ add_to_tree(branch, p)
25
+ else:
26
+ tree.add(p.name)
27
+
28
+
29
+ @app.command()
30
+ def tree() -> None:
31
+ """キャッシュディレクトリのツリー構造を表示します。"""
32
+ cache_dir = get_cache_dir()
33
+
34
+ if not cache_dir.exists():
35
+ typer.echo(f"キャッシュディレクトリ '{cache_dir}' は存在しません。")
36
+ return
37
+
38
+ console = Console()
39
+ tree_view = Tree(str(cache_dir))
40
+ add_to_tree(tree_view, cache_dir)
41
+ console.print(tree_view)
42
+
43
+
44
+ @app.command()
45
+ def clean() -> None:
46
+ """キャッシュディレクトリを削除します。"""
47
+ cache_dir = get_cache_dir()
48
+
49
+ if not cache_dir.exists():
50
+ typer.echo(f"キャッシュディレクトリ '{cache_dir}' は存在しません。")
51
+ return
52
+
53
+ try:
54
+ shutil.rmtree(cache_dir)
55
+ msg = f"キャッシュディレクトリ '{cache_dir}' を正常にクリーンアップしました。"
56
+ typer.echo(msg)
57
+ except OSError:
58
+ msg = f"キャッシュディレクトリ '{cache_dir}' のクリーンアップ中に"
59
+ msg += "エラーが発生しました。"
60
+ typer.secho(msg, fg=typer.colors.RED, bold=True)
61
+ raise typer.Exit(1) from None
@@ -57,7 +57,12 @@ async def _fetch(
57
57
 
58
58
  from kabukit.jquants.concurrent import fetch_all
59
59
 
60
- df = await fetch_all(target, progress=tqdm.asyncio.tqdm, **kwargs)
60
+ try:
61
+ df = await fetch_all(target, progress=tqdm.asyncio.tqdm, **kwargs)
62
+ except KeyboardInterrupt:
63
+ typer.echo("中断しました。")
64
+ raise typer.Exit(1) from None
65
+
61
66
  typer.echo(df)
62
67
  path = cls(df).write()
63
68
  typer.echo(f"全銘柄の{message}を '{path}' に保存しました。")
@@ -100,7 +105,12 @@ async def list_() -> None:
100
105
  from kabukit.core.list import List
101
106
  from kabukit.edinet.concurrent import fetch_list
102
107
 
103
- df = await fetch_list(years=10, progress=tqdm.asyncio.tqdm)
108
+ try:
109
+ df = await fetch_list(years=10, progress=tqdm.asyncio.tqdm)
110
+ except (KeyboardInterrupt, RuntimeError):
111
+ typer.echo("中断しました。")
112
+ raise typer.Exit(1) from None
113
+
104
114
  typer.echo(df)
105
115
  path = List(df).write()
106
116
  typer.echo(f"報告書一覧を '{path}' に保存しました。")
@@ -125,7 +135,12 @@ async def reports() -> None:
125
135
  lst = df.filter(pl.col("csvFlag"), pl.col("secCode").is_not_null())
126
136
  doc_ids = lst["docID"].unique()
127
137
 
128
- df = await fetch_csv(doc_ids, limit=1000, progress=tqdm.asyncio.tqdm)
138
+ try:
139
+ df = await fetch_csv(doc_ids, limit=1000, progress=tqdm.asyncio.tqdm)
140
+ except (KeyboardInterrupt, RuntimeError):
141
+ typer.echo("中断しました。")
142
+ raise typer.Exit(1) from None
143
+
129
144
  typer.echo(df)
130
145
  path = Reports(df).write()
131
146
  typer.echo(f"報告書を '{path}' に保存しました。")
@@ -1,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
- from pathlib import Path
5
4
  from typing import TYPE_CHECKING
6
5
 
7
6
  import polars as pl
8
- from platformdirs import user_cache_dir
7
+
8
+ from kabukit.utils.config import get_cache_dir
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Iterable
12
+ from pathlib import Path
12
13
  from typing import Any, Self
13
14
 
14
15
  from polars import DataFrame
@@ -24,7 +25,7 @@ class Base:
24
25
  @classmethod
25
26
  def data_dir(cls) -> Path:
26
27
  clsname = cls.__name__.lower()
27
- return Path(user_cache_dir("kabukit")) / clsname
28
+ return get_cache_dir() / clsname
28
29
 
29
30
  def write(self) -> Path:
30
31
  data_dir = self.data_dir()
@@ -12,7 +12,7 @@ from kabukit.core.client import Client
12
12
  from kabukit.utils.config import load_dotenv, set_key
13
13
  from kabukit.utils.params import get_params
14
14
 
15
- from . import info, prices, statements
15
+ from . import info, prices, statements, topix
16
16
 
17
17
  if TYPE_CHECKING:
18
18
  from collections.abc import AsyncIterator
@@ -342,3 +342,32 @@ class JQuantsClient(Client):
342
342
  return df
343
343
 
344
344
  return df.with_columns(pl.col("^.*Date$").str.to_date("%Y-%m-%d", strict=False))
345
+
346
+ async def get_topix(
347
+ self,
348
+ from_: str | datetime.date | None = None,
349
+ to: str | datetime.date | None = None,
350
+ ) -> DataFrame:
351
+ """日次のTOPIX指数データを取得する。
352
+
353
+ Args:
354
+ from_: 取得期間の開始日。
355
+ to: 取得期間の終了日。
356
+
357
+ Returns:
358
+ 日次のTOPIX指数データを含むPolars DataFrame。
359
+
360
+ Raises:
361
+ HTTPStatusError: APIリクエストが失敗した場合。
362
+ """
363
+ params = get_params(from_=from_, to=to)
364
+
365
+ url = "/indices/topix"
366
+ name = "topix"
367
+
368
+ dfs = [df async for df in self.iter_pages(url, params, name)]
369
+ df = pl.concat(dfs)
370
+ if df.is_empty():
371
+ return df
372
+
373
+ return topix.clean(df)
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
5
5
  from kabukit.utils import concurrent
6
6
 
7
7
  from .client import JQuantsClient
8
- from .info import get_codes
8
+ from .info import get_target_codes
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Iterable
@@ -80,7 +80,7 @@ async def fetch_all(
80
80
  すべての銘柄の財務情報を含む単一のDataFrame。
81
81
  """
82
82
 
83
- codes = await get_codes()
83
+ codes = await get_target_codes()
84
84
  codes = codes[:limit]
85
85
 
86
86
  return await fetch(
@@ -11,10 +11,13 @@ def clean(df: DataFrame) -> DataFrame:
11
11
  ).drop("^.+Code$", "CompanyNameEnglish")
12
12
 
13
13
 
14
- async def get_codes() -> list[str]:
15
- """銘柄コードのリストを返す。
14
+ async def get_target_codes() -> list[str]:
15
+ """分析対象となる銘柄コードのリストを返す。
16
16
 
17
- 市場「TOKYO PRO MARKET」と業種「その他」を除外した銘柄を対象とする。
17
+ 以下の条件を満たす銘柄は対象外とする。
18
+ - 市場: TOKYO PRO MARKET
19
+ - 業種: その他 -- (投資信託など)
20
+ - 優先株式
18
21
  """
19
22
  from .client import JQuantsClient
20
23
 
@@ -25,6 +28,7 @@ async def get_codes() -> list[str]:
25
28
  info.filter(
26
29
  pl.col("MarketCodeName") != "TOKYO PRO MARKET",
27
30
  pl.col("Sector17CodeName") != "その他",
31
+ ~pl.col("CompanyName").str.contains("優先株式"),
28
32
  )
29
33
  .get_column("Code")
30
34
  .to_list()
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import polars as pl
6
+
7
+ if TYPE_CHECKING:
8
+ from polars import DataFrame
9
+
10
+
11
+ def clean(df: DataFrame) -> DataFrame:
12
+ return df.select(
13
+ pl.col("Date").str.to_date("%Y-%m-%d"),
14
+ pl.lit("TOPIX").alias("Code"),
15
+ pl.exclude("Date"),
16
+ )
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- from dataclasses import dataclass
4
+ import contextlib
5
5
  from typing import TYPE_CHECKING, Any, Protocol
6
6
 
7
7
  import polars as pl
@@ -48,10 +48,17 @@ async def collect[R](
48
48
  async with semaphore:
49
49
  return await awaitable
50
50
 
51
- futures = (run(awaitable) for awaitable in awaitables)
51
+ tasks = {asyncio.create_task(run(awaitable)) for awaitable in awaitables}
52
52
 
53
- async for future in asyncio.as_completed(futures):
54
- yield await future
53
+ try:
54
+ async for future in asyncio.as_completed(tasks):
55
+ with contextlib.suppress(asyncio.CancelledError):
56
+ yield await future
57
+ finally:
58
+ for task in tasks:
59
+ task.cancel()
60
+ if tasks:
61
+ await asyncio.gather(*tasks, return_exceptions=True)
55
62
 
56
63
 
57
64
  async def collect_fn[T, R](
@@ -92,19 +99,16 @@ type Callback = Callable[[DataFrame], DataFrame | None]
92
99
  type Progress = type[progress_bar[Any] | tqdm[Any]] | _Progress
93
100
 
94
101
 
95
- @dataclass
96
- class Stream:
97
- cls: type[Client]
98
- resource: str
99
- args: list[Any]
100
- max_concurrency: int | None = None
101
-
102
- async def __aiter__(self) -> AsyncIterator[DataFrame]:
103
- async with self.cls() as client:
104
- fn = getattr(client, f"get_{self.resource}")
102
+ async def get_stream(
103
+ client: Client,
104
+ resource: str,
105
+ args: list[Any],
106
+ max_concurrency: int | None = None,
107
+ ) -> AsyncIterator[DataFrame]:
108
+ fn = getattr(client, f"get_{resource}")
105
109
 
106
- async for df in collect_fn(fn, self.args, self.max_concurrency):
107
- yield df
110
+ async for df in collect_fn(fn, args, max_concurrency):
111
+ yield df
108
112
 
109
113
 
110
114
  async def fetch(
@@ -137,13 +141,14 @@ async def fetch(
137
141
  すべての情報を含む単一のDataFrame。
138
142
  """
139
143
  args = list(args)
140
- stream = Stream(cls, resource, args, max_concurrency)
144
+ async with cls() as client:
145
+ stream = get_stream(client, resource, args, max_concurrency)
141
146
 
142
- if progress:
143
- stream = progress(aiter(stream), total=len(args))
147
+ if progress:
148
+ stream = progress(stream, total=len(args))
144
149
 
145
- if callback:
146
- stream = (x if (r := callback(x)) is None else r async for x in stream)
150
+ if callback:
151
+ stream = (x if (r := callback(x)) is None else r async for x in stream)
147
152
 
148
- dfs = [df async for df in stream if not df.is_empty()]
149
- return pl.concat(dfs) if dfs else pl.DataFrame()
153
+ dfs = [df async for df in stream if not df.is_empty()]
154
+ return pl.concat(dfs) if dfs else pl.DataFrame()
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
 
5
5
  import dotenv
6
- from platformdirs import user_config_dir
6
+ from platformdirs import user_cache_dir, user_config_dir
7
7
 
8
8
 
9
9
  def get_dotenv_path() -> Path:
@@ -21,3 +21,7 @@ def set_key(key: str, value: str) -> tuple[bool | None, str, str]:
21
21
  def load_dotenv() -> bool:
22
22
  dotenv_path = get_dotenv_path()
23
23
  return dotenv.load_dotenv(dotenv_path)
24
+
25
+
26
+ def get_cache_dir() -> Path:
27
+ return Path(user_cache_dir("kabukit"))
File without changes
File without changes
File without changes
File without changes
File without changes