kabukit 0.1.1__py3-none-any.whl → 0.2.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/__init__.py CHANGED
@@ -1,4 +1,7 @@
1
+ from .core.info import Info
2
+ from .core.prices import Prices
3
+ from .core.statements import Statements
1
4
  from .edinet.client import EdinetClient
2
5
  from .jquants.client import JQuantsClient
3
6
 
4
- __all__ = ["EdinetClient", "JQuantsClient"]
7
+ __all__ = ["EdinetClient", "Info", "JQuantsClient", "Prices", "Statements"]
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,57 @@
1
+ """チャート作成のためのモジュール"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ import altair as alt
8
+
9
+ if TYPE_CHECKING:
10
+ from kabukit.core.prices import Prices
11
+
12
+
13
+ def plot_prices(
14
+ prices: Prices,
15
+ kind: Literal["candlestick"] = "candlestick",
16
+ ) -> alt.VConcatChart:
17
+ if kind == "candlestick":
18
+ chart_price = plot_prices_candlestick(prices)
19
+ chart_price_volume = plot_prices_volume(prices)
20
+ return alt.vconcat(chart_price, chart_price_volume)
21
+
22
+ raise NotImplementedError # pyright: ignore[reportUnreachable]
23
+
24
+
25
+ def plot_prices_candlestick(prices: Prices) -> alt.LayerChart:
26
+ rule = alt.Chart(prices.data, mark="rule").encode(y="Low:Q", y2="High:Q")
27
+ bar = alt.Chart(prices.data, mark="bar").encode(y="Open:Q", y2="Close:Q")
28
+
29
+ color_condition = alt.condition(
30
+ "datum.Open < datum.Close",
31
+ alt.value("#ff3030"),
32
+ alt.value("#3030ff"),
33
+ )
34
+
35
+ return alt.layer(rule, bar, height=200).encode(
36
+ x=alt.X("Date:T", axis=alt.Axis(title="日付", format="%Y-%m-%d")),
37
+ y=alt.Y(title="株価", scale=alt.Scale(zero=False)),
38
+ color=color_condition,
39
+ tooltip=[
40
+ alt.Tooltip("Date:T", title="日付"),
41
+ alt.Tooltip("Open:Q", title="始値"),
42
+ alt.Tooltip("High:Q", title="高値"),
43
+ alt.Tooltip("Low:Q", title="安値"),
44
+ alt.Tooltip("Close:Q", title="終値"),
45
+ ],
46
+ )
47
+
48
+
49
+ def plot_prices_volume(prices: Prices) -> alt.Chart:
50
+ return alt.Chart(prices.data, mark="bar", height=50).encode(
51
+ x=alt.X("Date:T", axis=alt.Axis(title="日付", format="%Y-%m-%d")),
52
+ y=alt.Y("Volume:Q", title="出来高"),
53
+ tooltip=[
54
+ alt.Tooltip("Date:T", title="日付"),
55
+ alt.Tooltip("Volume:Q", title="出来高"),
56
+ ],
57
+ )
kabukit/cli/auth.py CHANGED
@@ -50,7 +50,7 @@ async def jquants_alias(mailaddress: Mailaddress, password: Password) -> None:
50
50
 
51
51
  def auth_edinet(api_key: str) -> None:
52
52
  """EDINET APIのAPIキーを設定ファイルに保存します。"""
53
- from kabukit.config import set_key
53
+ from kabukit.utils.config import set_key
54
54
 
55
55
  set_key("EDINET_API_KEY", api_key)
56
56
  typer.echo("EDINETのAPIキーを保存しました。")
@@ -75,7 +75,7 @@ def show() -> None:
75
75
  """設定ファイルに保存したトークン・APIキーを表示します。"""
76
76
  from dotenv import dotenv_values
77
77
 
78
- from kabukit.config import get_dotenv_path
78
+ from kabukit.utils.config import get_dotenv_path
79
79
 
80
80
  path = get_dotenv_path()
81
81
  typer.echo(f"Configuration file: {path}")
File without changes
kabukit/core/base.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ import polars as pl
8
+ from platformdirs import user_cache_dir
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Self
12
+
13
+ from polars import DataFrame
14
+
15
+
16
+ class Base:
17
+ data: DataFrame
18
+
19
+ def __init__(self, data: DataFrame) -> None:
20
+ self.data = data
21
+
22
+ @classmethod
23
+ def data_dir(cls) -> Path:
24
+ clsname = cls.__name__.lower()
25
+ return Path(user_cache_dir("kabukit")) / clsname
26
+
27
+ def write(self) -> Path:
28
+ data_dir = self.data_dir()
29
+ data_dir.mkdir(parents=True, exist_ok=True)
30
+ path = datetime.datetime.today().strftime("%Y%m%d") # noqa: DTZ002
31
+ filename = data_dir / f"{path}.parquet"
32
+ self.data.write_parquet(filename)
33
+ return filename
34
+
35
+ @classmethod
36
+ def read(cls, path: str | None = None) -> Self:
37
+ data_dir = cls.data_dir()
38
+
39
+ if path is None:
40
+ filename = sorted(data_dir.glob("*.parquet"))[-1]
41
+ else:
42
+ filename = data_dir / path
43
+
44
+ data = pl.read_parquet(filename)
45
+ return cls(data)
kabukit/core/client.py ADDED
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from httpx import AsyncClient
6
+
7
+ if TYPE_CHECKING:
8
+ from typing import Self
9
+
10
+
11
+ class Client:
12
+ client: AsyncClient
13
+
14
+ def __init__(self, base_url: str = "") -> None:
15
+ self.client = AsyncClient(base_url=base_url)
16
+
17
+ async def aclose(self) -> None:
18
+ """HTTPクライアントを閉じる。"""
19
+ await self.client.aclose()
20
+
21
+ async def __aenter__(self) -> Self:
22
+ return self
23
+
24
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] # noqa: ANN001
25
+ await self.aclose()
kabukit/core/info.py ADDED
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from .base import Base
6
+
7
+ if TYPE_CHECKING:
8
+ from polars import DataFrame
9
+
10
+
11
+ class Info(Base):
12
+ pass
kabukit/core/prices.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import polars as pl
6
+
7
+ from .base import Base
8
+
9
+ if TYPE_CHECKING:
10
+ from datetime import timedelta
11
+ from typing import Self
12
+
13
+ from polars import Expr
14
+
15
+
16
+ class Prices(Base):
17
+ def truncate(self, every: str | timedelta | Expr) -> Self:
18
+ data = (
19
+ self.data.group_by(pl.col("Date").dt.truncate(every), "Code")
20
+ .agg(
21
+ pl.col("Open").drop_nulls().first(),
22
+ pl.col("High").max(),
23
+ pl.col("Low").min(),
24
+ pl.col("Close").drop_nulls().last(),
25
+ pl.col("Volume").sum(),
26
+ pl.col("TurnoverValue").sum(),
27
+ )
28
+ .sort("Code", "Date")
29
+ )
30
+ return self.__class__(data)
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from .base import Base
4
+
5
+
6
+ class Statements(Base):
7
+ pass
@@ -0,0 +1,3 @@
1
+ from .concurrent import fetch, fetch_list
2
+
3
+ __all__ = ["fetch", "fetch_list"]
kabukit/edinet/client.py CHANGED
@@ -7,11 +7,13 @@ from enum import StrEnum
7
7
  from typing import TYPE_CHECKING
8
8
 
9
9
  import polars as pl
10
- from httpx import AsyncClient
11
10
  from polars import DataFrame
12
11
 
13
- from kabukit.config import load_dotenv
14
- from kabukit.params import get_params
12
+ from kabukit.core.client import Client
13
+ from kabukit.utils.config import load_dotenv
14
+ from kabukit.utils.params import get_params
15
+
16
+ from .doc import clean_csv, clean_list
15
17
 
16
18
  if TYPE_CHECKING:
17
19
  import datetime
@@ -29,11 +31,9 @@ class AuthKey(StrEnum):
29
31
  API_KEY = "EDINET_API_KEY"
30
32
 
31
33
 
32
- class EdinetClient:
33
- client: AsyncClient
34
-
34
+ class EdinetClient(Client):
35
35
  def __init__(self, api_key: str | None = None) -> None:
36
- self.client = AsyncClient(base_url=BASE_URL)
36
+ super().__init__(BASE_URL)
37
37
  self.set_api_key(api_key)
38
38
 
39
39
  def set_api_key(self, api_key: str | None = None) -> None:
@@ -44,9 +44,6 @@ class EdinetClient:
44
44
  if api_key:
45
45
  self.client.params = {"Subscription-Key": api_key}
46
46
 
47
- async def aclose(self) -> None:
48
- await self.client.aclose()
49
-
50
47
  async def get(self, url: str, params: QueryParamTypes) -> Response:
51
48
  resp = await self.client.get(url, params=params)
52
49
  resp.raise_for_status()
@@ -71,7 +68,12 @@ class EdinetClient:
71
68
  if "results" not in data:
72
69
  return DataFrame()
73
70
 
74
- return DataFrame(data["results"], infer_schema_length=None)
71
+ df = DataFrame(data["results"], infer_schema_length=None)
72
+
73
+ if df.is_empty():
74
+ return df
75
+
76
+ return clean_list(df, date)
75
77
 
76
78
  async def get_document(self, doc_id: str, doc_type: int) -> Response:
77
79
  params = get_params(type=doc_type)
@@ -105,7 +107,7 @@ class EdinetClient:
105
107
  f.read(),
106
108
  separator="\t",
107
109
  encoding="utf-16-le",
108
- )
110
+ ).pipe(clean_csv, doc_id)
109
111
 
110
112
  msg = "CSV is not available."
111
113
  raise ValueError(msg)
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING
5
+
6
+ from kabukit.utils import concurrent
7
+
8
+ from .client import EdinetClient
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import Iterable
12
+
13
+ from polars import DataFrame
14
+
15
+ from kabukit.utils.concurrent import Callback, Progress
16
+
17
+
18
+ def get_dates(days: int | None = None, years: int | None = None) -> list[datetime.date]:
19
+ """過去days日またはyears年の日付リストを返す。
20
+
21
+ Args:
22
+ days (int | None): 過去days日の日付リストを取得する。
23
+ years (int | None): 過去years年の日付リストを取得する。
24
+ daysが指定されている場合は無視される。
25
+ """
26
+ end_date = datetime.date.today() # noqa: DTZ011
27
+
28
+ if days is not None:
29
+ start_date = end_date - datetime.timedelta(days=days)
30
+ elif years is not None:
31
+ start_date = end_date.replace(year=end_date.year - years)
32
+ else:
33
+ msg = "daysまたはyearsのいずれかを指定してください。"
34
+ raise ValueError(msg)
35
+
36
+ return [
37
+ start_date + datetime.timedelta(days=i)
38
+ for i in range(1, (end_date - start_date).days + 1)
39
+ ]
40
+
41
+
42
+ async def fetch(
43
+ resource: str,
44
+ args: Iterable[str | datetime.date],
45
+ /,
46
+ max_concurrency: int | None = None,
47
+ progress: Progress | None = None,
48
+ callback: Callback | None = None,
49
+ ) -> DataFrame:
50
+ """引数に対応する各種データを取得し、単一のDataFrameにまとめて返す。
51
+
52
+ Args:
53
+ resource (str): 取得するデータの種類。EdinetClientのメソッド名から"get_"を
54
+ 除いたものを指定する。
55
+ args (Iterable[str | datetime.date]): 取得対象の引数のリスト。
56
+ max_concurrency (int | None, optional): 同時に実行するリクエストの最大数。
57
+ 指定しないときはデフォルト値が使用される。
58
+ progress (Progress | None, optional): 進捗表示のための関数。
59
+ tqdm, marimoなどのライブラリを使用できる。
60
+ 指定しないときは進捗表示は行われない。
61
+ callback (Callback | None, optional): 各DataFrameに対して適用する
62
+ コールバック関数。指定しないときはそのままのDataFrameが使用される。
63
+
64
+ Returns:
65
+ DataFrame:
66
+ すべての銘柄の財務情報を含む単一のDataFrame。
67
+ """
68
+ return await concurrent.fetch(
69
+ EdinetClient,
70
+ resource,
71
+ args,
72
+ max_concurrency=max_concurrency,
73
+ progress=progress,
74
+ callback=callback,
75
+ )
76
+
77
+
78
+ async def fetch_list(
79
+ days: int | None = None,
80
+ years: int | None = None,
81
+ limit: int | None = None,
82
+ max_concurrency: int | None = None,
83
+ progress: Progress | None = None,
84
+ callback: Callback | None = None,
85
+ ) -> DataFrame:
86
+ """過去days日またはyears年の文書一覧を取得し、単一のDataFrameにまとめて返す。
87
+
88
+ Args:
89
+ days (int | None): 過去days日の日付リストを取得する。
90
+ years (int | None): 過去years年の日付リストを取得する。
91
+ daysが指定されている場合は無視される。
92
+ max_concurrency (int | None, optional): 同時に実行するリクエストの最大数。
93
+ 指定しないときはデフォルト値が使用される。
94
+ progress (Progress | None, optional): 進捗表示のための関数。
95
+ tqdm, marimoなどのライブラリを使用できる。
96
+ 指定しないときは進捗表示は行われない。
97
+ callback (Callback | None, optional): 各DataFrameに対して適用する
98
+ コールバック関数。指定しないときはそのままのDataFrameが使用される。
99
+
100
+ Returns:
101
+ DataFrame:
102
+ 文書一覧を含む単一のDataFrame。
103
+ """
104
+ dates = get_dates(days=days, years=years)
105
+
106
+ if limit is not None:
107
+ dates = dates[:limit]
108
+
109
+ return await fetch(
110
+ "list",
111
+ dates,
112
+ max_concurrency=max_concurrency,
113
+ progress=progress,
114
+ callback=callback,
115
+ )
116
+
117
+
118
+ async def fetch_csv(
119
+ doc_ids: Iterable[str],
120
+ /,
121
+ limit: int | None = None,
122
+ max_concurrency: int | None = None,
123
+ progress: Progress | None = None,
124
+ callback: Callback | None = None,
125
+ ) -> DataFrame:
126
+ """文書をCSV形式で取得し、単一のDataFrameにまとめて返す。
127
+
128
+ Args:
129
+ doc_ids (Iterable[str]): 取得対象の文書IDのリスト。
130
+ max_concurrency (int | None, optional): 同時に実行するリクエストの最大数。
131
+ 指定しないときはデフォルト値が使用される。
132
+ progress (Progress | None, optional): 進捗表示のための関数。
133
+ tqdm, marimoなどのライブラリを使用できる。
134
+ 指定しないときは進捗表示は行われない。
135
+ callback (Callback | None, optional): 各DataFrameに対して適用する
136
+ コールバック関数。指定しないときはそのままのDataFrameが使用される。
137
+
138
+ Returns:
139
+ DataFrame:
140
+ 文書含む単一のDataFrame。
141
+ """
142
+ doc_ids = list(doc_ids)
143
+
144
+ if limit is not None:
145
+ doc_ids = doc_ids[:limit]
146
+
147
+ return await fetch(
148
+ "csv",
149
+ doc_ids,
150
+ max_concurrency=max_concurrency,
151
+ progress=progress,
152
+ callback=callback,
153
+ )
kabukit/edinet/doc.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from typing import TYPE_CHECKING
5
+
6
+ import polars as pl
7
+
8
+ if TYPE_CHECKING:
9
+ from polars import DataFrame
10
+
11
+
12
+ def clean_list(df: DataFrame, date: str | datetime.date) -> DataFrame:
13
+ if isinstance(date, str):
14
+ date = datetime.datetime.strptime(date, "%Y-%m-%d").date() # noqa: DTZ007
15
+
16
+ return df.with_columns(
17
+ pl.lit(date).alias("Date"),
18
+ pl.col("submitDateTime").str.to_datetime("%Y-%m-%d %H:%M", strict=False),
19
+ pl.col("^period.+$").str.to_date("%Y-%m-%d", strict=False),
20
+ pl.col("^.+Flag$").cast(pl.Int8).cast(pl.Boolean),
21
+ pl.col("^.+Code$").cast(pl.String),
22
+ pl.col("opeDateTime")
23
+ .cast(pl.String)
24
+ .str.to_datetime("%Y-%m-%d %H:%M", strict=False),
25
+ ).select("Date", pl.exclude("Date"))
26
+
27
+
28
+ def clean_csv(df: DataFrame, doc_id: str) -> DataFrame:
29
+ return df.select(
30
+ pl.lit(doc_id).alias("docID"),
31
+ pl.all(),
32
+ )
@@ -1,3 +1,3 @@
1
- from .stream import fetch, fetch_all
1
+ from .concurrent import fetch, fetch_all
2
2
 
3
3
  __all__ = ["fetch", "fetch_all"]