kabukit 0.3.0__tar.gz → 0.4.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 (45) hide show
  1. {kabukit-0.3.0 → kabukit-0.4.0}/PKG-INFO +2 -3
  2. {kabukit-0.3.0 → kabukit-0.4.0}/pyproject.toml +8 -10
  3. kabukit-0.4.0/src/kabukit/__init__.py +17 -0
  4. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/cli/auth.py +2 -2
  5. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/cli/get.py +56 -6
  6. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/core/base.py +12 -1
  7. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/core/client.py +1 -1
  8. kabukit-0.4.0/src/kabukit/core/list.py +12 -0
  9. kabukit-0.4.0/src/kabukit/core/prices.py +92 -0
  10. kabukit-0.4.0/src/kabukit/core/reports.py +12 -0
  11. kabukit-0.4.0/src/kabukit/core/statements.py +24 -0
  12. kabukit-0.4.0/src/kabukit/edinet/__init__.py +3 -0
  13. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/edinet/client.py +3 -7
  14. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/edinet/concurrent.py +4 -2
  15. kabukit-0.4.0/src/kabukit/edinet/doc.py +46 -0
  16. kabukit-0.4.0/src/kabukit/jquants/__init__.py +4 -0
  17. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/jquants/client.py +7 -2
  18. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/jquants/concurrent.py +1 -1
  19. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/jquants/schema.py +6 -9
  20. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/jquants/statements.py +28 -15
  21. kabukit-0.3.0/src/kabukit/__init__.py +0 -7
  22. kabukit-0.3.0/src/kabukit/core/prices.py +0 -30
  23. kabukit-0.3.0/src/kabukit/core/statements.py +0 -7
  24. kabukit-0.3.0/src/kabukit/edinet/__init__.py +0 -3
  25. kabukit-0.3.0/src/kabukit/edinet/doc.py +0 -32
  26. kabukit-0.3.0/src/kabukit/jquants/__init__.py +0 -3
  27. {kabukit-0.3.0 → kabukit-0.4.0}/LICENSE +0 -0
  28. {kabukit-0.3.0 → kabukit-0.4.0}/README.md +0 -0
  29. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/analysis/__init__.py +0 -0
  30. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/analysis/indicators.py +0 -0
  31. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/analysis/preprocess.py +0 -0
  32. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/analysis/screener.py +0 -0
  33. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/analysis/visualization.py +0 -0
  34. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/cli/__init__.py +0 -0
  35. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/cli/app.py +0 -0
  36. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/core/__init__.py +0 -0
  37. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/core/info.py +0 -0
  38. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/jquants/info.py +0 -0
  39. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/jquants/prices.py +0 -0
  40. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/py.typed +0 -0
  41. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/utils/__init__.py +0 -0
  42. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/utils/concurrent.py +0 -0
  43. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/utils/config.py +0 -0
  44. {kabukit-0.3.0 → kabukit-0.4.0}/src/kabukit/utils/date.py +0 -0
  45. {kabukit-0.3.0 → kabukit-0.4.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.3.0
3
+ Version: 0.4.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>
@@ -28,10 +28,9 @@ License: MIT License
28
28
  Classifier: Development Status :: 4 - Beta
29
29
  Classifier: Programming Language :: Python
30
30
  Classifier: Programming Language :: Python :: 3.13
31
- Requires-Dist: altair>=5
32
31
  Requires-Dist: async-typer>=0.1
33
32
  Requires-Dist: holidays>=0.81
34
- Requires-Dist: httpx>=0.28.1
33
+ Requires-Dist: httpx>=0.28
35
34
  Requires-Dist: platformdirs>=4
36
35
  Requires-Dist: polars>=1
37
36
  Requires-Dist: python-dotenv>=1
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "kabukit"
7
- version = "0.3.0"
7
+ version = "0.4.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" }
@@ -16,10 +16,9 @@ classifiers = [
16
16
  ]
17
17
  requires-python = ">=3.13"
18
18
  dependencies = [
19
- "altair>=5",
20
19
  "async-typer>=0.1",
21
20
  "holidays>=0.81",
22
- "httpx>=0.28.1",
21
+ "httpx>=0.28",
23
22
  "platformdirs>=4",
24
23
  "polars>=1",
25
24
  "python-dotenv>=1",
@@ -36,6 +35,7 @@ Issues = "https://github.com/daizutabi/kabukit/issues"
36
35
 
37
36
  [dependency-groups]
38
37
  dev = [
38
+ "altair>=5",
39
39
  "basedpyright>=1.31.4",
40
40
  "marimo[lsp]>=0.16",
41
41
  "numpy>=2.3.3", # polars 1.33 type hinting workaround,
@@ -54,14 +54,12 @@ dev = [
54
54
  docs = ["mkapi>=4.4", "mkdocs-marimo", "mkdocs-material"]
55
55
 
56
56
  [tool.pytest.ini_options]
57
- addopts = [
58
- "--cov=kabukit",
59
- "--cov-report=lcov:lcov.info",
60
- "--doctest-modules",
61
- "-m not integration",
57
+ addopts = ["--cov=kabukit", "--cov-report=lcov:lcov.info", "--doctest-modules"]
58
+ testpaths = ["tests/unit", "tests/integration", "tests/validation"]
59
+ markers = [
60
+ "integration: marks tests as integration tests",
61
+ "validation: marks tests as data validation tests",
62
62
  ]
63
- testpaths = ["tests/unit", "tests/integration"]
64
- markers = ["integration: marks tests as integration tests"]
65
63
 
66
64
  [tool.coverage.report]
67
65
  exclude_lines = ["no cov", "raise NotImplementedError", "if TYPE_CHECKING:"]
@@ -0,0 +1,17 @@
1
+ from .core.info import Info
2
+ from .core.list import List
3
+ from .core.prices import Prices
4
+ from .core.reports import Reports
5
+ from .core.statements import Statements
6
+ from .edinet.client import EdinetClient
7
+ from .jquants.client import JQuantsClient
8
+
9
+ __all__ = [
10
+ "EdinetClient",
11
+ "Info",
12
+ "JQuantsClient",
13
+ "List",
14
+ "Prices",
15
+ "Reports",
16
+ "Statements",
17
+ ]
@@ -20,8 +20,8 @@ async def auth_jquants(mailaddress: str, password: str) -> None:
20
20
  async with JQuantsClient() as client:
21
21
  try:
22
22
  await client.auth(mailaddress, password, save=True)
23
- except HTTPStatusError as e:
24
- typer.echo(f"認証に失敗しました: {e}")
23
+ except HTTPStatusError:
24
+ typer.echo("認証に失敗しました。")
25
25
  raise Exit(1) from None
26
26
 
27
27
  typer.echo("J-Quantsのリフレッシュトークン・IDトークンを保存しました。")
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Annotated, Any
3
+ from typing import TYPE_CHECKING, Annotated, Any
4
4
 
5
5
  import typer
6
6
  from async_typer import AsyncTyper # pyright: ignore[reportMissingTypeStubs]
7
7
  from typer import Argument
8
8
 
9
+ if TYPE_CHECKING:
10
+ from kabukit.core.base import Base
11
+
9
12
  app = AsyncTyper(
10
13
  add_completion=False,
11
14
  help="J-Quantsからデータを取得します。",
@@ -36,7 +39,7 @@ async def info(code: Code = None) -> None:
36
39
  async def _fetch(
37
40
  code: str | None,
38
41
  target: str,
39
- writer_cls: type,
42
+ cls: type[Base],
40
43
  fetch_func_name: str,
41
44
  message: str,
42
45
  **kwargs: Any,
@@ -56,7 +59,7 @@ async def _fetch(
56
59
 
57
60
  df = await fetch_all(target, progress=tqdm.asyncio.tqdm, **kwargs)
58
61
  typer.echo(df)
59
- path = writer_cls(df).write()
62
+ path = cls(df).write()
60
63
  typer.echo(f"全銘柄の{message}を '{path}' に保存しました。")
61
64
 
62
65
 
@@ -68,7 +71,7 @@ async def statements(code: Code = None) -> None:
68
71
  await _fetch(
69
72
  code=code,
70
73
  target="statements",
71
- writer_cls=Statements,
74
+ cls=Statements,
72
75
  fetch_func_name="get_statements",
73
76
  message="財務情報",
74
77
  )
@@ -82,16 +85,55 @@ async def prices(code: Code = None) -> None:
82
85
  await _fetch(
83
86
  code=code,
84
87
  target="prices",
85
- writer_cls=Prices,
88
+ cls=Prices,
86
89
  fetch_func_name="get_prices",
87
90
  message="株価情報",
88
91
  max_concurrency=8,
89
92
  )
90
93
 
91
94
 
95
+ @app.async_command(name="list") # pyright: ignore[reportUnknownMemberType]
96
+ async def list_() -> None:
97
+ """報告書一覧を取得します。"""
98
+ import tqdm.asyncio
99
+
100
+ from kabukit.core.list import List
101
+ from kabukit.edinet.concurrent import fetch_list
102
+
103
+ df = await fetch_list(years=10, progress=tqdm.asyncio.tqdm)
104
+ typer.echo(df)
105
+ path = List(df).write()
106
+ typer.echo(f"報告書一覧を '{path}' に保存しました。")
107
+
108
+
109
+ @app.async_command() # pyright: ignore[reportUnknownMemberType]
110
+ async def reports() -> None:
111
+ """報告書を取得します。"""
112
+ import polars as pl
113
+ import tqdm.asyncio
114
+
115
+ from kabukit.core.list import List
116
+ from kabukit.core.reports import Reports
117
+ from kabukit.edinet.concurrent import fetch_csv
118
+
119
+ try:
120
+ df = List.read().data
121
+ except FileNotFoundError:
122
+ await list_()
123
+ df = List.read().data
124
+
125
+ lst = df.filter(pl.col("csvFlag"), pl.col("secCode").is_not_null())
126
+ doc_ids = lst["docID"].unique()
127
+
128
+ df = await fetch_csv(doc_ids, limit=1000, progress=tqdm.asyncio.tqdm)
129
+ typer.echo(df)
130
+ path = Reports(df).write()
131
+ typer.echo(f"報告書を '{path}' に保存しました。")
132
+
133
+
92
134
  @app.async_command(name="all") # pyright: ignore[reportUnknownMemberType]
93
135
  async def all_(code: Code = None) -> None:
94
- """上場銘柄一覧、財務情報、株価を連続して取得します。"""
136
+ """上場銘柄一覧、財務情報、株価、報告書を連続して取得します。"""
95
137
  typer.echo("上場銘柄一覧を取得します。")
96
138
  await info(code)
97
139
 
@@ -102,3 +144,11 @@ async def all_(code: Code = None) -> None:
102
144
  typer.echo("---")
103
145
  typer.echo("株価を取得します。")
104
146
  await prices(code)
147
+
148
+ if code is None:
149
+ typer.echo("---")
150
+ typer.echo("報告書一覧を取得します。")
151
+ await list_()
152
+ typer.echo("---")
153
+ typer.echo("報告書を取得します。")
154
+ await reports()
@@ -8,9 +8,11 @@ import polars as pl
8
8
  from platformdirs import user_cache_dir
9
9
 
10
10
  if TYPE_CHECKING:
11
- from typing import Self
11
+ from collections.abc import Iterable
12
+ from typing import Any, Self
12
13
 
13
14
  from polars import DataFrame
15
+ from polars._typing import IntoExprColumn
14
16
 
15
17
 
16
18
  class Base:
@@ -48,3 +50,12 @@ class Base:
48
50
 
49
51
  data = pl.read_parquet(filename)
50
52
  return cls(data)
53
+
54
+ def filter(
55
+ self,
56
+ *predicates: IntoExprColumn | Iterable[IntoExprColumn] | bool | list[bool],
57
+ **constraints: Any,
58
+ ) -> Self:
59
+ """Filter the data with given predicates and constraints."""
60
+ data = self.data.filter(*predicates, **constraints)
61
+ return self.__class__(data)
@@ -12,7 +12,7 @@ class Client:
12
12
  client: AsyncClient
13
13
 
14
14
  def __init__(self, base_url: str = "") -> None:
15
- self.client = AsyncClient(base_url=base_url)
15
+ self.client = AsyncClient(base_url=base_url, timeout=20)
16
16
 
17
17
  async def aclose(self) -> None:
18
18
  """HTTPクライアントを閉じる。"""
@@ -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 List(Base):
12
+ pass
@@ -0,0 +1,92 @@
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
+ from .statements import Statements
16
+
17
+
18
+ class Prices(Base):
19
+ def truncate(self, every: str | timedelta | Expr) -> Self:
20
+ data = (
21
+ self.data.group_by(pl.col("Date").dt.truncate(every), "Code")
22
+ .agg(
23
+ pl.col("Open").drop_nulls().first(),
24
+ pl.col("High").max(),
25
+ pl.col("Low").min(),
26
+ pl.col("Close").drop_nulls().last(),
27
+ pl.col("Volume").sum(),
28
+ pl.col("TurnoverValue").sum(),
29
+ )
30
+ .sort("Code", "Date")
31
+ )
32
+ return self.__class__(data)
33
+
34
+ def with_adjusted_shares(self, statements: Statements) -> Self:
35
+ """日次の調整済み株式数を計算し、列として追加する。
36
+
37
+ 決算短信で報告される株式数(例:発行済株式総数)は、四半期ごとなど
38
+ 特定の日付のデータです。一方で、株式分割や併合は日々発生し、株式数を
39
+ 変動させます。
40
+ このメソッドは、直近の決算で報告された株式数を、日々の調整係数
41
+ (`AdjustmentFactor`) を用いて補正し、日次ベースの時系列データとして
42
+ 提供します。これにより、日々の時価総額計算などが正確に行えるようになります。
43
+
44
+ 具体的には、`statements`から`TotalShares`(発行済株式総数)と
45
+ `TreasuryShares`(自己株式数)を取得し、それぞれを調整します。
46
+ 計算結果は、元の列名との混同を避けるため、接頭辞`Adjusted`を付与した
47
+ 新しい列(`AdjustedTotalShares`, `AdjustedTreasuryShares`)として
48
+ 追加されます。
49
+
50
+ .. note::
51
+ この計算は、決算発表間の株式数の変動が、株式分割・併合
52
+ (`AdjustmentFactor`)にのみ起因すると仮定しています。
53
+ 期中に行われる増資や自己株式取得など、`AdjustmentFactor`に
54
+ 反映されないイベントによる株式数の変動は考慮されません。
55
+
56
+ Args:
57
+ statements (Statements): `number_of_shares()`メソッドを通じて
58
+ 株式数データを提供できる`Statements`オブジェクト。
59
+
60
+ Returns:
61
+ Self: `AdjustedTotalShares`および`AdjustedTreasuryShares`列が
62
+ 追加された、新しいPricesオブジェクト。
63
+ """
64
+ shares = statements.number_of_shares().rename({"Date": "ReportDate"})
65
+
66
+ adjusted = (
67
+ self.data.join_asof(
68
+ shares,
69
+ left_on="Date",
70
+ right_on="ReportDate",
71
+ by="Code",
72
+ check_sortedness=False,
73
+ )
74
+ .with_columns(
75
+ (1.0 / pl.col("AdjustmentFactor"))
76
+ .cum_prod()
77
+ .over("Code", "ReportDate")
78
+ .alias("CumulativeRatio"),
79
+ )
80
+ .select(
81
+ "Date",
82
+ "Code",
83
+ (pl.col("TotalShares", "TreasuryShares") * pl.col("CumulativeRatio"))
84
+ .round(0)
85
+ .cast(pl.Int64)
86
+ .name.prefix("Adjusted"),
87
+ )
88
+ )
89
+
90
+ data = self.data.join(adjusted, on=["Date", "Code"], how="left")
91
+
92
+ return self.__class__(data)
@@ -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 Reports(Base):
12
+ pass
@@ -0,0 +1,24 @@
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 polars import DataFrame
11
+
12
+
13
+ class Statements(Base):
14
+ def number_of_shares(self) -> DataFrame:
15
+ """発行済株式数を取得する。"""
16
+ return self.data.filter(
17
+ pl.col("TotalShares").is_not_null(),
18
+ ).select(
19
+ "Date",
20
+ "Code",
21
+ "TotalShares",
22
+ "TreasuryShares",
23
+ "AverageOutstandingShares",
24
+ )
@@ -0,0 +1,3 @@
1
+ from .concurrent import fetch, fetch_csv, fetch_list
2
+
3
+ __all__ = ["fetch", "fetch_csv", "fetch_list"]
@@ -6,14 +6,13 @@ import zipfile
6
6
  from enum import StrEnum
7
7
  from typing import TYPE_CHECKING
8
8
 
9
- import polars as pl
10
9
  from polars import DataFrame
11
10
 
12
11
  from kabukit.core.client import Client
13
12
  from kabukit.utils.config import load_dotenv
14
13
  from kabukit.utils.params import get_params
15
14
 
16
- from .doc import clean_csv, clean_list
15
+ from .doc import clean_csv, clean_list, read_csv
17
16
 
18
17
  if TYPE_CHECKING:
19
18
  import datetime
@@ -103,11 +102,8 @@ class EdinetClient(Client):
103
102
  for info in zf.infolist():
104
103
  if info.filename.endswith(".csv"):
105
104
  with zf.open(info) as f:
106
- return pl.read_csv(
107
- f.read(),
108
- separator="\t",
109
- encoding="utf-16-le",
110
- ).pipe(clean_csv, doc_id)
105
+ df = read_csv(f.read())
106
+ return clean_csv(df, doc_id)
111
107
 
112
108
  msg = "CSV is not available."
113
109
  raise ValueError(msg)
@@ -83,13 +83,14 @@ async def fetch_list(
83
83
  if limit is not None:
84
84
  dates = dates[:limit]
85
85
 
86
- return await fetch(
86
+ df = await fetch(
87
87
  "list",
88
88
  dates,
89
89
  max_concurrency=max_concurrency,
90
90
  progress=progress,
91
91
  callback=callback,
92
92
  )
93
+ return df.sort("Date")
93
94
 
94
95
 
95
96
  async def fetch_csv(
@@ -121,10 +122,11 @@ async def fetch_csv(
121
122
  if limit is not None:
122
123
  doc_ids = doc_ids[:limit]
123
124
 
124
- return await fetch(
125
+ df = await fetch(
125
126
  "csv",
126
127
  doc_ids,
127
128
  max_concurrency=max_concurrency,
128
129
  progress=progress,
129
130
  callback=callback,
130
131
  )
132
+ return df.sort("docID")
@@ -0,0 +1,46 @@
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
+ null_columns = [c for c in df.columns if df[c].dtype == pl.Null]
17
+
18
+ return (
19
+ df.with_columns(
20
+ pl.col(null_columns).cast(pl.String),
21
+ )
22
+ .with_columns(
23
+ pl.lit(date).alias("Date"),
24
+ pl.col("^.+DateTime$").str.to_datetime("%Y-%m-%d %H:%M", strict=False),
25
+ pl.col("^period.+$").str.to_date("%Y-%m-%d", strict=False),
26
+ pl.col("^.+Flag$").cast(pl.Int8).cast(pl.Boolean),
27
+ pl.col("^.+Code$").cast(pl.String),
28
+ )
29
+ .select("Date", pl.exclude("Date"))
30
+ )
31
+
32
+
33
+ def read_csv(data: bytes) -> DataFrame:
34
+ return pl.read_csv(
35
+ data,
36
+ separator="\t",
37
+ encoding="utf-16-le",
38
+ infer_schema_length=None,
39
+ )
40
+
41
+
42
+ def clean_csv(df: DataFrame, doc_id: str) -> DataFrame:
43
+ return df.select(
44
+ pl.lit(doc_id).alias("docID"),
45
+ pl.all(),
46
+ )
@@ -0,0 +1,4 @@
1
+ from .concurrent import fetch, fetch_all
2
+ from .schema import rename
3
+
4
+ __all__ = ["fetch", "fetch_all", "rename"]
@@ -257,6 +257,7 @@ class JQuantsClient(Client):
257
257
  date: str | datetime.date | None = None,
258
258
  *,
259
259
  clean: bool = True,
260
+ with_date: bool = True,
260
261
  ) -> DataFrame:
261
262
  """四半期毎の決算短信サマリーおよび業績・配当の修正に関する開示情報を取得する。
262
263
 
@@ -264,6 +265,8 @@ class JQuantsClient(Client):
264
265
  code (str, optional): 財務情報を取得する銘柄のコード。
265
266
  date (str | datetime.date, optional): 財務情報を取得する日付。
266
267
  clean (bool, optional): 取得したデータをクリーンアップするかどうか。
268
+ with_date (bool, optional): クリーンアップ後に営業日ベースで開示日の翌日を
269
+ 計算して`Date`列を追加するかどうか。
267
270
 
268
271
  Returns:
269
272
  財務情報を含むDataFrame。
@@ -283,10 +286,12 @@ class JQuantsClient(Client):
283
286
  dfs = [df async for df in self.iter_pages(url, params, name)]
284
287
  df = pl.concat(dfs)
285
288
 
286
- if df.is_empty():
289
+ if df.is_empty() or not clean:
287
290
  return df
288
291
 
289
- return statements.clean(df) if clean else df
292
+ df = statements.clean(df)
293
+
294
+ return statements.with_date(df) if with_date else df
290
295
 
291
296
  async def get_announcement(self) -> DataFrame:
292
297
  """翌日発表予定の決算情報を取得する。
@@ -23,7 +23,7 @@ async def fetch(
23
23
  progress: Progress | None = None,
24
24
  callback: Callback | None = None,
25
25
  ) -> DataFrame:
26
- """全銘柄の各種データを取得し、単一のDataFrameにまとめて返す。
26
+ """複数の銘柄の各種データを取得し、単一のDataFrameにまとめて返す。
27
27
 
28
28
  Args:
29
29
  resource (str): 取得するデータの種類。JQuantsClientのメソッド名から"get_"を
@@ -45,8 +45,9 @@ class PriceColumns(BaseColumns):
45
45
 
46
46
 
47
47
  class StatementColumns(BaseColumns):
48
- Date = "開示日"
49
- Time = "開示時刻"
48
+ Date = "日付"
49
+ DisclosedDate = "開示日"
50
+ DisclosedTime = "開示時刻"
50
51
  Code = "銘柄コード"
51
52
  DisclosureNumber = "開示番号"
52
53
  TypeOfDocument = "開示書類種別"
@@ -129,13 +130,10 @@ class StatementColumns(BaseColumns):
129
130
  ChangesInAccountingEstimates = "会計上の見積りの変更"
130
131
  RetrospectiveRestatement = "修正再表示"
131
132
 
132
- # NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock
133
- NumberOfShares = "期末発行済株式数"
134
- # NumberOfTreasuryStockAtTheEndOfFiscalYear
135
- NumberOfTreasuryStock = "期末自己株式数"
136
- AverageNumberOfShares = "期中平均株式数"
133
+ TotalShares = "期末発行済株式数" # 自己株式を含む (NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock)
134
+ TreasuryShares = "期末自己株式数" # (NumberOfTreasuryStockAtTheEndOfFiscalYear)
135
+ AverageOutstandingShares = "期中平均株式数" # 自己株式を除く。EPSなどの計算に使用される (AverageNumberOfShares)
137
136
 
138
- """
139
137
  NonConsolidatedNetSales = "売上高_非連結"
140
138
  NonConsolidatedOperatingProfit = "営業利益_非連結"
141
139
  NonConsolidatedOrdinaryProfit = "経常利益_非連結"
@@ -170,7 +168,6 @@ class StatementColumns(BaseColumns):
170
168
  NextYearForecastNonConsolidatedOrdinaryProfit = "経常利益_予想_翌事業年度期末_非連結"
171
169
  NextYearForecastNonConsolidatedProfit = "当期純利益_予想_翌事業年度期末_非連結"
172
170
  NextYearForecastNonConsolidatedEarningsPerShare = "一株あたり当期純利益_予想_翌事業年度期末_非連結"
173
- """
174
171
 
175
172
 
176
173
  def rename(df: DataFrame, *, strict: bool = False) -> DataFrame:
@@ -13,19 +13,18 @@ if TYPE_CHECKING:
13
13
 
14
14
  def clean(df: DataFrame) -> DataFrame:
15
15
  return (
16
- df.select(pl.exclude(r"^.*\(REIT\)|.*NonConsolidated.*$"))
16
+ df.select(pl.exclude(r"^.*\(REIT\)$"))
17
17
  .rename(
18
18
  {
19
- "DisclosedDate": "Date",
20
- "DisclosedTime": "Time",
21
19
  "LocalCode": "Code",
22
- "NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock": "NumberOfShares", # noqa: E501
23
- "NumberOfTreasuryStockAtTheEndOfFiscalYear": "NumberOfTreasuryStock",
20
+ "NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock": "TotalShares", # noqa: E501
21
+ "NumberOfTreasuryStockAtTheEndOfFiscalYear": "TreasuryShares",
22
+ "AverageNumberOfShares": "AverageOutstandingShares",
24
23
  },
25
24
  )
26
25
  .with_columns(
27
26
  pl.col("^.*Date$").str.to_date("%Y-%m-%d", strict=False),
28
- pl.col("Time").str.to_time("%H:%M:%S", strict=False),
27
+ pl.col("DisclosedTime").str.to_time("%H:%M:%S", strict=False),
29
28
  pl.col("TypeOfCurrentPeriod").cast(pl.Categorical),
30
29
  )
31
30
  .pipe(_cast_float)
@@ -45,10 +44,17 @@ def _cast_float(df: DataFrame) -> DataFrame:
45
44
  "Earnings",
46
45
  "Equity",
47
46
  "NetSales",
48
- "NumberOf",
49
47
  "PayoutRatio",
50
48
  "Profit",
51
49
  ]
50
+ ).with_columns(
51
+ pl.col(
52
+ "TotalShares",
53
+ "TreasuryShares",
54
+ ).cast(pl.Int64, strict=False),
55
+ pl.col(
56
+ "AverageOutstandingShares",
57
+ ).cast(pl.Float64, strict=False),
52
58
  )
53
59
 
54
60
 
@@ -77,15 +83,22 @@ def get_holidays(year: int | None = None, n: int = 10) -> list[datetime.date]:
77
83
  return sorted(dates.keys())
78
84
 
79
85
 
80
- def update_effective_date(df: DataFrame, year: int | None = None) -> DataFrame:
81
- """開示日が休日や15時以降の場合、翌営業日に更新する。"""
82
- holidays = get_holidays(year=year)
86
+ def with_date(df: DataFrame, year: int | None = None) -> DataFrame:
87
+ """`Date`列を追加する。
83
88
 
84
- cond = pl.col("Time").is_null() | (pl.col("Time") > datetime.time(15, 0))
89
+ 開示日が休日のとき、あるいは、開示時刻が15時以降の場合、Dateを開示日の翌営業日に設定する。
90
+ """
91
+ is_after_hours = pl.col("DisclosedTime").is_null() | (
92
+ pl.col("DisclosedTime") > datetime.time(15, 0)
93
+ )
85
94
 
86
- return df.with_columns(
87
- pl.when(cond)
88
- .then(pl.col("Date").dt.add_business_days(1, holidays=holidays))
89
- .otherwise(pl.col("Date"))
95
+ holidays = get_holidays(year=year)
96
+
97
+ return df.select(
98
+ pl.when(is_after_hours)
99
+ .then(pl.col("DisclosedDate") + datetime.timedelta(days=1))
100
+ .otherwise(pl.col("DisclosedDate"))
101
+ .dt.add_business_days(0, holidays=holidays, roll="forward")
90
102
  .alias("Date"),
103
+ pl.all(),
91
104
  )
@@ -1,7 +0,0 @@
1
- from .core.info import Info
2
- from .core.prices import Prices
3
- from .core.statements import Statements
4
- from .edinet.client import EdinetClient
5
- from .jquants.client import JQuantsClient
6
-
7
- __all__ = ["EdinetClient", "Info", "JQuantsClient", "Prices", "Statements"]
@@ -1,30 +0,0 @@
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)
@@ -1,7 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from .base import Base
4
-
5
-
6
- class Statements(Base):
7
- pass
@@ -1,3 +0,0 @@
1
- from .concurrent import fetch, fetch_list
2
-
3
- __all__ = ["fetch", "fetch_list"]
@@ -1,32 +0,0 @@
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 +0,0 @@
1
- from .concurrent import fetch, fetch_all
2
-
3
- __all__ = ["fetch", "fetch_all"]
File without changes
File without changes
File without changes
File without changes