BackcastPro 0.3.4__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.
@@ -0,0 +1,384 @@
1
+ import requests
2
+ import os
3
+ from datetime import datetime
4
+ import pandas as pd
5
+ import logging
6
+ import threading
7
+
8
+ # 環境変数を読み込み
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ logger = logging.getLogger(__name__)
14
+ logger.setLevel(logging.INFO)
15
+
16
+ DEFAULT_TIMEOUT_SECONDS = 10
17
+
18
+
19
+ class jquants:
20
+ """
21
+ J-Quants API Client (Singleton)
22
+ """
23
+
24
+ _instance = None
25
+ _lock = threading.Lock()
26
+
27
+ def __new__(cls):
28
+ if cls._instance is None:
29
+ with cls._lock:
30
+ if cls._instance is None:
31
+ cls._instance = super(jquants, cls).__new__(cls)
32
+ return cls._instance
33
+
34
+ def __init__(self):
35
+ # 既に初期化済みの場合はスキップ
36
+ if hasattr(self, "_initialized"):
37
+ return
38
+
39
+ self.API_URL = "https://api.jquants.com"
40
+ self.api_key = os.getenv("JQUANTS_API_KEY")
41
+ self.headers = {}
42
+ self._initialized = True
43
+ self.isEnable = self._set_api_key()
44
+ if self.isEnable:
45
+ self.headers = {"x-api-key": self.api_key}
46
+
47
+ def _set_api_key(self) -> bool:
48
+ """
49
+ APIキーを設定
50
+ """
51
+ if not self.api_key:
52
+ logger.warning("J-QuantsのAPIキーが設定されていません。")
53
+ return False
54
+ logger.info("API使用の準備が完了しました。")
55
+ return True
56
+
57
+ def _ensure_api_key(self) -> bool:
58
+ """
59
+ APIキーが利用可能かを確認する
60
+ """
61
+ if self.isEnable:
62
+ return True
63
+ self.api_key = os.getenv("JQUANTS_API_KEY")
64
+ if not self.api_key:
65
+ return False
66
+ self.headers = {"x-api-key": self.api_key}
67
+ self.isEnable = True
68
+ return True
69
+
70
+ def _handle_auth_error(self, res: requests.Response) -> None:
71
+ if res.status_code in (401, 403):
72
+ logger.error(f"API認証エラー: {res.status_code}")
73
+ self.isEnable = False
74
+
75
+ def _get_all_pages(self, endpoint: str, params: dict) -> list:
76
+ if not self._ensure_api_key():
77
+ return []
78
+
79
+ data: list = []
80
+ page_params = dict(params)
81
+ while True:
82
+ res = requests.get(
83
+ f"{self.API_URL}{endpoint}",
84
+ params=page_params,
85
+ headers=self.headers,
86
+ timeout=DEFAULT_TIMEOUT_SECONDS,
87
+ )
88
+ if res.status_code in (401, 403):
89
+ self._handle_auth_error(res)
90
+ break
91
+ if res.status_code != 200:
92
+ try:
93
+ logger.error(f"API Error: {res.status_code} - {res.json()}")
94
+ except Exception:
95
+ logger.error(f"API Error: {res.status_code} - {res.text}")
96
+ break
97
+
98
+ d = res.json()
99
+ data += d.get("data", [])
100
+ pagination_key = d.get("pagination_key")
101
+ if pagination_key:
102
+ page_params["pagination_key"] = pagination_key
103
+ continue
104
+ break
105
+
106
+ return data
107
+
108
+ def get_listed_info(self, code="", date="") -> pd.DataFrame:
109
+ """
110
+ 上場銘柄一覧(/v2/equities/master)
111
+
112
+ - 過去時点での銘柄情報、当日の銘柄情報および翌営業日時点の銘柄情報が取得可能です。
113
+ - データの取得では、銘柄コード(code)または日付(date)の指定が可能です。
114
+
115
+ (データ更新時刻)
116
+ - 毎営業日の24:00頃
117
+
118
+ """
119
+ if isinstance(date, datetime):
120
+ date = date.strftime("%Y-%m-%d")
121
+
122
+ params = {}
123
+ if code != "":
124
+ # codeが4文字だったら末尾に0を付けて
125
+ if len(code) == 4:
126
+ code = code + "0"
127
+ params["code"] = code
128
+ if date != "":
129
+ params["date"] = date
130
+
131
+ data = self._get_all_pages("/v2/equities/master", params)
132
+ if not data:
133
+ return pd.DataFrame()
134
+
135
+ df = pd.DataFrame(data)
136
+ if "Code" not in df.columns and "code" in df.columns:
137
+ df = df.rename(columns={"code": "Code"})
138
+ if "CompanyName" not in df.columns and "Name" in df.columns:
139
+ df = df.rename(columns={"Name": "CompanyName"})
140
+ df["source"] = "j-quants"
141
+
142
+ return df
143
+
144
+ def get_daily_quotes(
145
+ self, code: str, from_: datetime = None, to: datetime = None
146
+ ) -> pd.DataFrame:
147
+ """
148
+ 株価四本値(/v2/equities/bars/daily)
149
+
150
+ - 株価は分割・併合を考慮した調整済み株価(小数点第2位四捨五入)と調整前の株価を取得することができます。
151
+ - データの取得では、銘柄コード(code)または日付(date)の指定が必須となります。
152
+
153
+ (データ更新時刻)
154
+ - 毎営業日の17:00頃
155
+
156
+ - Premiumプランの方には、日通しに加え、前場(Morning)及び後場(Afternoon)の四本値及び取引高(調整前・後両方)・取引代金が取得可能です。
157
+ - データの取得では、日付(date)を指定して全銘柄取得するモードがあるが、非対応となっています。
158
+ """
159
+ # V2では code または date が必須のため code を要求する
160
+ if not code or not str(code).strip():
161
+ raise ValueError("銘柄コードが指定されていません")
162
+
163
+ # codeが4文字だったら末尾に0を付けて
164
+ if len(code) == 4:
165
+ code = code + "0"
166
+
167
+ params = {}
168
+ if code != "":
169
+ params["code"] = code
170
+ if from_ is not None:
171
+ # 文字列形式の日付も対応
172
+ if isinstance(from_, str):
173
+ from_ = datetime.strptime(from_, "%Y-%m-%d")
174
+ params["from"] = from_.strftime("%Y-%m-%d")
175
+ if to is not None:
176
+ # 文字列形式の日付も対応
177
+ if isinstance(to, str):
178
+ to = datetime.strptime(to, "%Y-%m-%d")
179
+ params["to"] = to.strftime("%Y-%m-%d")
180
+
181
+ data = self._get_all_pages("/v2/equities/bars/daily", params)
182
+ if not data:
183
+ return pd.DataFrame()
184
+
185
+ df = pd.DataFrame(data)
186
+ df = _rename_daily_quote_columns(df)
187
+ # 型変換(日次株価フィールド定義に基づく)
188
+ df = _normalize_columns(df)
189
+ df["source"] = "j-quants"
190
+
191
+ return df
192
+
193
+ def get_fins_statements(self, code="", date="", from_="", to="") -> pd.DataFrame:
194
+ """
195
+ 財務情報(/v2/fins/summary)
196
+
197
+ - 財務情報APIでは、上場企業がTDnetへ提出する決算短信Summary等を基に作成された、四半期毎の財務情報を取得することができます。
198
+ - データの取得では、銘柄コード(code)または開示日(date)の指定が必須です。
199
+
200
+ (データ更新時刻)
201
+ - 速報18:00頃、確報24:30頃
202
+ """
203
+ params = {}
204
+ if code != "":
205
+ # codeが4文字だったら末尾に0を付けて
206
+ if len(code) == 4:
207
+ code = code + "0"
208
+ params["code"] = code
209
+ if date != "":
210
+ params["date"] = date
211
+ if from_ != "":
212
+ params["from"] = from_
213
+ if to != "":
214
+ params["to"] = to
215
+
216
+ data = self._get_all_pages("/v2/fins/summary", params)
217
+ if not data:
218
+ return pd.DataFrame()
219
+
220
+ df = pd.DataFrame(data)
221
+ df["source"] = "j-quants"
222
+
223
+ return df
224
+
225
+ def get_fins_announcement(self) -> pd.DataFrame:
226
+ """
227
+ 決算発表予定日(/v2/equities/earnings-calendar)
228
+
229
+ (データ更新時刻)
230
+ - 不定期(更新がある日は)19:00頃
231
+
232
+ - [当該ページ](https://www.jpx.co.jp/listing/event-schedules/financial-announcement/index.html)で、3月期・9月期決算会社分に更新があった場合のみ19時ごろに更新されます。
233
+ """
234
+ params = {}
235
+ data = self._get_all_pages("/v2/equities/earnings-calendar", params)
236
+ if not data:
237
+ return pd.DataFrame()
238
+
239
+ df = pd.DataFrame(data)
240
+ df["source"] = "j-quants"
241
+
242
+ return df
243
+
244
+ def get_market_trading_calendar(
245
+ self, holidaydivision="", from_="", to=""
246
+ ) -> pd.DataFrame:
247
+ """
248
+ 取引カレンダー(/v2/markets/calendar)
249
+
250
+ - 東証およびOSEにおける営業日、休業日、ならびにOSEにおける祝日取引の有無の情報を取得できます。
251
+ - データの取得では、休日区分(holidaydivision)または日付(from/to)の指定が可能です。
252
+
253
+ (データ更新日)
254
+ - 不定期(原則として、毎年2月頃をめどに翌年1年間の営業日および祝日取引実施日(予定)を更新します。)
255
+ """
256
+ params = {}
257
+ if holidaydivision != "":
258
+ params["hol_div"] = holidaydivision
259
+ if from_ != "":
260
+ params["from"] = from_
261
+ if to != "":
262
+ params["to"] = to
263
+
264
+ data = self._get_all_pages("/v2/markets/calendar", params)
265
+ if not data:
266
+ return pd.DataFrame()
267
+
268
+ df = pd.DataFrame(data)
269
+ df["source"] = "j-quants"
270
+
271
+ return df
272
+
273
+
274
+ def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
275
+ """
276
+ カラム名をJ-Quants APIの形式に統一し、型変換を行う
277
+
278
+ 日次株価のフィールド定義に基づいた型変換:
279
+ - Date: string (YYYY-MM-DD) → DatetimeIndex
280
+ - Code: string → string
281
+ - 数値フィールド: number → float
282
+ (Open, High, Low, Close, Volume, TurnoverValue,
283
+ UpperLimit, LowerLimit, AdjustmentFactor,
284
+ AdjustmentOpen, AdjustmentHigh, AdjustmentLow,
285
+ AdjustmentClose, AdjustmentVolume)
286
+ """
287
+ # 既にDatetimeIndexの場合はそのまま処理を続行
288
+ if isinstance(df.index, pd.DatetimeIndex):
289
+ pass
290
+ # Date列をdatetime型に変換してindexに設定
291
+ elif "Date" in df.columns:
292
+ df["Date"] = pd.to_datetime(df["Date"])
293
+ df = df.set_index("Date")
294
+ else:
295
+ # インデックスが日付でない場合は、警告を出す
296
+ logger.warning("Dateカラムが存在せず、インデックスも日付型ではありません")
297
+
298
+ # Code列はstring型として保持(明示的に変換)
299
+ if "Code" in df.columns:
300
+ df["Code"] = df["Code"].astype(str)
301
+
302
+ # 数値フィールドの定義
303
+ numeric_fields = [
304
+ "Open",
305
+ "High",
306
+ "Low",
307
+ "Close",
308
+ "Volume",
309
+ "TurnoverValue",
310
+ "UpperLimit",
311
+ "LowerLimit",
312
+ "AdjustmentFactor",
313
+ "AdjustmentOpen",
314
+ "AdjustmentHigh",
315
+ "AdjustmentLow",
316
+ "AdjustmentClose",
317
+ "AdjustmentVolume",
318
+ ]
319
+
320
+ # DataFrameに存在する数値フィールドのみ変換
321
+ for field in numeric_fields:
322
+ if field in df.columns:
323
+ df[field] = pd.to_numeric(df[field], errors="coerce")
324
+
325
+ # カラムの順序を統一(Dateはindexなので含めない)
326
+ column_order = [
327
+ "Code",
328
+ "Open",
329
+ "High",
330
+ "Low",
331
+ "Close",
332
+ "UpperLimit",
333
+ "LowerLimit",
334
+ "Volume",
335
+ "TurnoverValue",
336
+ "AdjustmentFactor",
337
+ "AdjustmentOpen",
338
+ "AdjustmentHigh",
339
+ "AdjustmentLow",
340
+ "AdjustmentClose",
341
+ "AdjustmentVolume",
342
+ ]
343
+ # 存在するカラムのみを選択
344
+ available_columns = [col for col in column_order if col in df.columns]
345
+ # 存在しないカラムも含める(順序は保持)
346
+ all_columns = available_columns + [
347
+ col for col in df.columns if col not in column_order
348
+ ]
349
+ df = df[all_columns].copy()
350
+
351
+ return df
352
+
353
+
354
+ def _rename_daily_quote_columns(df: pd.DataFrame) -> pd.DataFrame:
355
+ """
356
+ V2日次株価の短縮カラムを既存の列名へ変換
357
+ """
358
+ rename_map = {
359
+ "O": "Open",
360
+ "H": "High",
361
+ "L": "Low",
362
+ "C": "Close",
363
+ "Vo": "Volume",
364
+ "V": "Volume",
365
+ "Va": "TurnoverValue",
366
+ "TV": "TurnoverValue",
367
+ "UL": "UpperLimit",
368
+ "LL": "LowerLimit",
369
+ "AdjFactor": "AdjustmentFactor",
370
+ "AdjO": "AdjustmentOpen",
371
+ "AdjH": "AdjustmentHigh",
372
+ "AdjL": "AdjustmentLow",
373
+ "AdjC": "AdjustmentClose",
374
+ "AdjVo": "AdjustmentVolume",
375
+ "AdjV": "AdjustmentVolume",
376
+ }
377
+ columns = {k: v for k, v in rename_map.items() if k in df.columns}
378
+ if columns:
379
+ df = df.rename(columns=columns)
380
+ if "Code" not in df.columns and "code" in df.columns:
381
+ df = df.rename(columns={"code": "Code"})
382
+ if "Date" not in df.columns and "date" in df.columns:
383
+ df = df.rename(columns={"date": "Date"})
384
+ return df
@@ -0,0 +1,222 @@
1
+ import json
2
+ import urllib.request
3
+ import urllib.error
4
+ import os
5
+ import logging
6
+ import threading
7
+ from typing import Optional
8
+ from datetime import datetime, timedelta
9
+ import pandas as pd
10
+
11
+ # 環境変数を読み込み
12
+ from dotenv import load_dotenv
13
+ load_dotenv()
14
+
15
+ logger = logging.getLogger(__name__)
16
+ logger.setLevel(logging.INFO)
17
+
18
+ class kabusap:
19
+ """
20
+ kabuステーションAPI Client (Singleton)
21
+ """
22
+
23
+ _instance = None
24
+ _lock = threading.Lock()
25
+
26
+ def __new__(cls):
27
+ if cls._instance is None:
28
+ with cls._lock:
29
+ if cls._instance is None:
30
+ cls._instance = super(kabusap, cls).__new__(cls)
31
+ return cls._instance
32
+
33
+ def __init__(self):
34
+ # 既に初期化済みの場合はスキップ
35
+ if hasattr(self, '_initialized'):
36
+ return
37
+
38
+ self.API_URL = "http://localhost:18080/kabusapi"
39
+ self.api_key = ""
40
+ self.headers = {} # 初期化を確実にする
41
+ self._initialized = True
42
+ self.isEnable = self._set_token()
43
+ if self.isEnable:
44
+ self.headers = {
45
+ 'Content-Type': 'application/json',
46
+ 'X-API-KEY': self.api_key
47
+ }
48
+
49
+ def _set_token(self) -> bool:
50
+ """
51
+ APIトークンを取得
52
+
53
+ 正しく設定ファイルが作成されていれば、本コードを実行することで、APIトークンを取得することができます。
54
+ 「APIを使用する準備が完了しました。」と出力されれば、kabuステーションAPIをコールすることができるようになります!
55
+ """
56
+ api_password = os.getenv('KABUSAP_API_PASSWORD')
57
+
58
+ # 環境変数が設定されていない場合はAPI呼び出しを行わない
59
+ if not api_password:
60
+ logger.warning("kabuステーションAPIの認証情報(KABUSAP_API_PASSWORD)が設定されていません。")
61
+ return False
62
+
63
+ # トークン取得
64
+ try:
65
+ obj = {'APIPassword': api_password}
66
+ json_data = json.dumps(obj).encode('utf8')
67
+
68
+ url = f'{self.API_URL}/token'
69
+ req = urllib.request.Request(url, json_data, method='POST')
70
+ req.add_header('Content-Type', 'application/json')
71
+
72
+ with urllib.request.urlopen(req) as res:
73
+ content = json.loads(res.read())
74
+ # レスポンスからトークンを取得
75
+ # レスポンス形式は {'ResultCode': 0, 'Token': '...'} の形式を想定
76
+ if 'Token' in content:
77
+ self.api_key = content['Token']
78
+ logger.info("API使用の準備が完了しました。")
79
+ return True
80
+ else:
81
+ logger.error(f"トークンの取得に失敗しました。レスポンス: {content}")
82
+ return False
83
+ except urllib.error.HTTPError as e:
84
+ error_content = json.loads(e.read().decode('utf-8'))
85
+ logger.error(f"HTTPエラー: {e.code} - {error_content}")
86
+ return False
87
+ except Exception as e:
88
+ logger.error(f"トークンの取得に失敗しました: {e}")
89
+ return False
90
+
91
+ def _refresh_token_if_needed(self) -> bool:
92
+ """
93
+ トークンが期限切れの場合は再取得する
94
+ kabuステーションAPIのトークンは有効期限があるため、必要に応じて再取得する
95
+ """
96
+ # 現在の実装では、トークンが無効になった場合に再取得する
97
+ # 必要に応じて、トークンの有効期限をチェックするロジックを追加可能
98
+ if not self.api_key:
99
+ logger.info("トークンが無効のため、再取得します。")
100
+ return self._set_token()
101
+ return True
102
+
103
+ def get_board(self, code: str) -> pd.DataFrame:
104
+ """
105
+ 板情報を取得する
106
+
107
+ Args:
108
+ code (str): 銘柄コード
109
+
110
+ Returns:
111
+ pd.DataFrame: 板情報
112
+ """
113
+ # APIが有効でない場合は空のDataFrameを返す
114
+ if not self.isEnable:
115
+ logger.warning("kabuステーションAPIが有効ではありません。")
116
+ return pd.DataFrame()
117
+
118
+ # トークンリフレッシュが必要かチェック
119
+ self._refresh_token_if_needed()
120
+
121
+ # 銘柄コードの検証
122
+ if not code or not isinstance(code, str) or not code.strip():
123
+ logger.error("銘柄コードが指定されていません。")
124
+ return pd.DataFrame()
125
+
126
+ # 板情報取得のURLを構築(銘柄コード@市場コードの形式)
127
+ # 市場コード1は東証を表す
128
+ url = f'{self.API_URL}/board/{code}@1'
129
+
130
+ try:
131
+ # GETリクエストを送信
132
+ req = urllib.request.Request(url, method='GET')
133
+ req.add_header('Content-Type', 'application/json')
134
+ req.add_header('X-API-KEY', self.api_key)
135
+
136
+ with urllib.request.urlopen(req) as res:
137
+ content = json.loads(res.read())
138
+
139
+ # エラーチェック
140
+ if 'ResultCode' in content and content['ResultCode'] != 0:
141
+ logger.error(f"API Error: {content.get('ResultCode')} - {content.get('Message', '')}")
142
+ return pd.DataFrame()
143
+
144
+ # 板情報をDataFrameに変換
145
+ # APIレスポンスの構造に応じてデータを抽出
146
+ board_data = []
147
+
148
+ # パターン1: Bid/Askキーが存在する場合(JSON配列形式)
149
+ if 'Bid' in content and isinstance(content['Bid'], list):
150
+ for bid in content['Bid']:
151
+ board_data.append({
152
+ 'Price': bid.get('Price', 0),
153
+ 'Qty': bid.get('Qty', 0),
154
+ 'Type': 'Bid'
155
+ })
156
+
157
+ if 'Ask' in content and isinstance(content['Ask'], list):
158
+ for ask in content['Ask']:
159
+ board_data.append({
160
+ 'Price': ask.get('Price', 0),
161
+ 'Qty': ask.get('Qty', 0),
162
+ 'Type': 'Ask'
163
+ })
164
+
165
+ # パターン2: Sell1.Price, Buy1.Price形式の場合(json_normalize後の形式)
166
+ if not board_data:
167
+ normalized_df = pd.json_normalize(content)
168
+
169
+ # 買い板(Buy1~Buy10)の処理
170
+ for i in range(1, 11):
171
+ price_col = f'Buy{i}.Price'
172
+ qty_col = f'Buy{i}.Qty'
173
+ if price_col in normalized_df.columns and qty_col in normalized_df.columns:
174
+ price = normalized_df[price_col].iloc[0] if len(normalized_df) > 0 else 0
175
+ qty = normalized_df[qty_col].iloc[0] if len(normalized_df) > 0 else 0
176
+ if pd.notna(price) and pd.notna(qty) and price > 0 and qty > 0:
177
+ board_data.append({
178
+ 'Price': float(price),
179
+ 'Qty': int(qty),
180
+ 'Type': 'Bid'
181
+ })
182
+
183
+ # 売り板(Sell1~Sell10)の処理
184
+ for i in range(1, 11):
185
+ price_col = f'Sell{i}.Price'
186
+ qty_col = f'Sell{i}.Qty'
187
+ if price_col in normalized_df.columns and qty_col in normalized_df.columns:
188
+ price = normalized_df[price_col].iloc[0] if len(normalized_df) > 0 else 0
189
+ qty = normalized_df[qty_col].iloc[0] if len(normalized_df) > 0 else 0
190
+ if pd.notna(price) and pd.notna(qty) and price > 0 and qty > 0:
191
+ board_data.append({
192
+ 'Price': float(price),
193
+ 'Qty': int(qty),
194
+ 'Type': 'Ask'
195
+ })
196
+
197
+ # DataFrameに変換
198
+ if board_data:
199
+ df = pd.DataFrame(board_data)
200
+ # ソース情報を追加
201
+ df['source'] = 'kabu-station'
202
+ df['code'] = code
203
+ return df
204
+ else:
205
+ # 板情報が取得できなかった場合
206
+ logger.warning(f"板情報が取得できませんでした: {code}")
207
+ return pd.DataFrame()
208
+
209
+ except urllib.error.HTTPError as e:
210
+ error_content = json.loads(e.read().decode('utf-8'))
211
+ logger.error(f"HTTPエラー: {e.code} - {error_content}")
212
+ return pd.DataFrame()
213
+ except Exception as e:
214
+ logger.error(f"板情報の取得に失敗しました: {e}")
215
+ return pd.DataFrame()
216
+
217
+
218
+
219
+ if __name__ == '__main__':
220
+ kabusap = kabusap()
221
+ df = kabusap.get_board('8306')
222
+ print(df)