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,409 @@
1
+ import pandas as pd
2
+ import requests
3
+ from .util import PRICE_LIMIT_TABLE
4
+ try:
5
+ import yfinance as yf
6
+ except ImportError:
7
+ yf = None
8
+
9
+ from datetime import datetime, timedelta
10
+ from typing import Tuple
11
+
12
+ from .jquants import _normalize_columns
13
+
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+ logger.setLevel(logging.INFO)
18
+
19
+
20
+ def stooq_daily_quotes(code: str, from_: datetime = None, to: datetime = None) -> pd.DataFrame:
21
+ """
22
+ 株価データを取得する(Yahoo Finance API経由)
23
+
24
+ Args:
25
+ code (str): 銘柄コード(例: '7203')
26
+ from_ (datetime): データ取得開始日
27
+ to (datetime): データ取得終了日
28
+
29
+ Returns:
30
+ pd.DataFrame: 株価データのDataFrame(DatetimeIndexとして日付がインデックスに設定)
31
+ """
32
+ try:
33
+ # yfinance ライブラリが利用可能な場合はそれを使用
34
+ if yf is not None:
35
+ start = None if from_ is None else from_.strftime('%Y-%m-%d')
36
+ end = None if to is None else to.strftime('%Y-%m-%d')
37
+ df = yf.download(f"{code}.T", start, end, progress=False)
38
+ if not df.empty:
39
+ # データを日付昇順に並び替え
40
+ df = df.sort_index()
41
+
42
+ # DatetimeIndexであることを保証
43
+ if not isinstance(df.index, pd.DatetimeIndex):
44
+ if 'Date' in df.columns:
45
+ df['Date'] = pd.to_datetime(df['Date'])
46
+ df = df.set_index('Date')
47
+ else:
48
+ df.index = pd.to_datetime(df.index)
49
+ df.index.name = 'Date'
50
+
51
+ return df
52
+
53
+ # yfinance が利用できない場合は直接 Yahoo Finance API を使用
54
+ df = _get_yfinance_daily_quotes(code, from_, to)
55
+
56
+ if df.empty:
57
+ return pd.DataFrame()
58
+
59
+ # データを日付昇順に並び替え
60
+ df = df.sort_index()
61
+
62
+ # DatetimeIndexであることを保証(_get_yfinance_daily_quotesは既にDatetimeIndex)
63
+ if not isinstance(df.index, pd.DatetimeIndex):
64
+ if 'Date' in df.columns:
65
+ df['Date'] = pd.to_datetime(df['Date'])
66
+ df = df.set_index('Date')
67
+ else:
68
+ df.index = pd.to_datetime(df.index)
69
+ df.index.name = 'Date'
70
+
71
+ return df
72
+
73
+ except Exception as e:
74
+ logger.error(f"データ取得中にエラーが発生しました: {e}")
75
+ return pd.DataFrame()
76
+
77
+
78
+ def _stooq_normalize_columns(code: str, df: pd.DataFrame) -> pd.DataFrame:
79
+ """
80
+ カラム名をJ-Quants APIの形式に統一する
81
+
82
+ Args:
83
+ df (pd.DataFrame): 元のDataFrame
84
+
85
+ Returns:
86
+ pd.DataFrame: カラム名を統一したDataFrame
87
+ """
88
+ # Stooqのカラム名をJ-Quantsの形式にマッピング
89
+ names_mapping = {
90
+ 'Open': 'Open',
91
+ 'High': 'High',
92
+ 'Low': 'Low',
93
+ 'Close': 'Close',
94
+ 'Volume': 'Volume',
95
+ "Adj Close": "AdjustmentClose"
96
+ }
97
+
98
+ return _common_normalize_columns(code, df, names_mapping)
99
+
100
+
101
+ def _common_normalize_columns(code: str, df: pd.DataFrame, names_mapping: dict) -> pd.DataFrame:
102
+ """
103
+ カラム名をJ-Quants APIの形式に統一する(共通処理)
104
+
105
+ Args:
106
+ code (str): 銘柄コード
107
+ df (pd.DataFrame): 元のDataFrame
108
+ names_mapping (dict): カラム名のマッピング辞書
109
+
110
+ Returns:
111
+ pd.DataFrame: カラム名を統一したDataFrame
112
+ """
113
+ # Dateカラムが存在しない場合、インデックスから作成
114
+ if 'Date' not in df.columns:
115
+ if isinstance(df.index, pd.DatetimeIndex):
116
+ # インデックスが日付の場合は、Dateカラムとして追加
117
+ norm_df = df.copy()
118
+ norm_df['Date'] = norm_df.index
119
+ else:
120
+ # インデックスが日付でない場合は、そのまま使用
121
+ norm_df = df.copy()
122
+ if 'Date' not in norm_df.columns:
123
+ norm_df['Date'] = norm_df.index
124
+ else:
125
+ norm_df = df.copy()
126
+
127
+ # 必要なカラムのみを選択(Dateカラムは必ず含める)
128
+ column_mapping = {key: value for key, value in names_mapping.items() if key in norm_df.columns}
129
+
130
+ # Dateカラムを保持しつつ、他のカラムをマッピング
131
+ selected_columns = ['Date'] + [col for col in column_mapping.keys() if col != 'Date']
132
+ norm_df = norm_df[selected_columns].copy()
133
+ norm_df = norm_df.rename(columns=column_mapping)
134
+
135
+ # 数値フィールドの定義
136
+ numeric_fields = [value for value in column_mapping.values() if value != 'Date']
137
+
138
+ # DataFrameに存在する数値フィールドのみ変換
139
+ for field in numeric_fields:
140
+ if field in norm_df.columns:
141
+ norm_df[field] = pd.to_numeric(norm_df[field], errors='coerce')
142
+
143
+ # Codeカラムを追加
144
+ norm_df['Code'] = code
145
+
146
+ # TurnoverValue | 売買代金(出来高×価格の合計)カラムを追加
147
+ if not 'TurnoverValue' in norm_df.columns:
148
+ norm_df['TurnoverValue'] = norm_df['Volume'] * norm_df['Close']
149
+
150
+ # AdjustmentFactor | 株式分割等を考慮した調整係数
151
+ # AdjustmentOpen/High/Low | 調整済価格
152
+ norm_df = _add_adjustment_prices(norm_df)
153
+
154
+ # UpperLimit / LowerLimit | 制限値(前日終値を基準に計算)
155
+ norm_df = _add_price_limits(norm_df)
156
+
157
+ # 型変換を行う
158
+ norm_df = _normalize_columns(norm_df)
159
+
160
+ # Dateカラムが存在する場合は、それをインデックスに設定(DatetimeIndexに変換)
161
+ if 'Date' in norm_df.columns:
162
+ # Dateカラムを確実にdatetime型に変換
163
+ date_values = pd.to_datetime(norm_df['Date'], errors='coerce')
164
+ # Dateカラムを更新(変換後の値を使用)
165
+ norm_df['Date'] = date_values
166
+ # Dateカラムをインデックスに設定
167
+ norm_df = norm_df.set_index('Date')
168
+ # インデックスを明示的にDatetimeIndexに変換
169
+ # インデックスが既にDatetimeIndexでない場合に変換
170
+ if not isinstance(norm_df.index, pd.DatetimeIndex):
171
+ norm_df.index = pd.DatetimeIndex(pd.to_datetime(norm_df.index, errors='coerce'))
172
+ # 念のため、再度DatetimeIndexであることを確認
173
+ if not isinstance(norm_df.index, pd.DatetimeIndex):
174
+ # 最終手段:インデックスをDatetimeIndexとして再作成
175
+ norm_df.index = pd.DatetimeIndex(norm_df.index)
176
+ elif isinstance(norm_df.index, pd.DatetimeIndex):
177
+ # Dateカラムがないが、インデックスが既にDatetimeIndexの場合はそのまま使用
178
+ pass
179
+ else:
180
+ # Dateカラムもなく、インデックスもDatetimeIndexでない場合は変換を試みる
181
+ try:
182
+ converted_index = pd.to_datetime(norm_df.index, errors='coerce')
183
+ if converted_index.notna().any():
184
+ norm_df.index = pd.DatetimeIndex(converted_index)
185
+ else:
186
+ import warnings
187
+ warnings.warn("インデックスをDatetimeIndexに変換できませんでした。", stacklevel=2)
188
+ except (ValueError, TypeError) as e:
189
+ import warnings
190
+ warnings.warn(f"インデックスをDatetimeIndexに変換できませんでした: {e}", stacklevel=2)
191
+
192
+ # 最終確認:インデックスがDatetimeIndexであることを保証
193
+ if not isinstance(norm_df.index, pd.DatetimeIndex) and len(norm_df) > 0:
194
+ # 最終手段として、インデックスを DatetimeIndex に変換を試みる
195
+ try:
196
+ norm_df.index = pd.DatetimeIndex(pd.to_datetime(norm_df.index, errors='coerce'))
197
+ except (ValueError, TypeError):
198
+ import warnings
199
+ warnings.warn(f"最終的なDataFrameのインデックスがDatetimeIndexではありません。型: {type(norm_df.index)}", stacklevel=2)
200
+
201
+ return norm_df
202
+
203
+
204
+ def _add_adjustment_prices(df: pd.DataFrame) -> pd.DataFrame:
205
+ """
206
+ 株式分割等を考慮した調整係数と調整済価格を計算する
207
+
208
+ Args:
209
+ df (pd.DataFrame): 元のDataFrame(Open, High, Low, Close, Adj Close を含む)
210
+
211
+ Returns:
212
+ pd.DataFrame: 調整係数と調整済価格を追加したDataFrame
213
+ """
214
+ result_df = df.copy()
215
+
216
+ # AdjustmentClose が存在しない場合は Close と同じ値を使用
217
+ if not 'AdjustmentClose' in result_df.columns:
218
+ result_df['AdjustmentClose'] = result_df['Close']
219
+
220
+ # 調整係数を計算: AdjustmentFactor = Adj Close / Close
221
+ if not 'AdjustmentFactor' in result_df.columns:
222
+ result_df['AdjustmentFactor'] = result_df['AdjustmentClose'] / result_df['Close']
223
+
224
+ # 調整済価格を計算
225
+ if not 'AdjustmentOpen' in result_df.columns:
226
+ result_df['AdjustmentOpen'] = result_df['Open'] * result_df['AdjustmentFactor']
227
+ if not 'AdjustmentHigh' in result_df.columns:
228
+ result_df['AdjustmentHigh'] = result_df['High'] * result_df['AdjustmentFactor']
229
+ if not 'AdjustmentLow' in result_df.columns:
230
+ result_df['AdjustmentLow'] = result_df['Low'] * result_df['AdjustmentFactor']
231
+
232
+ # 調整済出来高を計算(株式分割時は出来高も調整が必要)
233
+ if not 'AdjustmentVolume' in result_df.columns:
234
+ result_df['AdjustmentVolume'] = result_df['Volume'] / result_df['AdjustmentFactor']
235
+
236
+ return result_df
237
+
238
+
239
+ def _add_price_limits(df: pd.DataFrame) -> pd.DataFrame:
240
+ """
241
+ DataFrameに値幅制限(ストップ高・ストップ安)を追加する
242
+
243
+ Args:
244
+ df: 株価データのDataFrame('Close'カラムを含む必要がある)
245
+
246
+ Returns:
247
+ 'UpperLimit'と'LowerLimit'カラムを追加したDataFrame
248
+ """
249
+
250
+ if 'UpperLimit' in df.columns and 'LowerLimit' in df.columns:
251
+ return df
252
+
253
+ def _get_price_limit_range(price: float, expansion: int = 1) -> tuple[float, float]:
254
+ """
255
+ 東証の値幅制限(ストップ高・ストップ安)を計算する
256
+
257
+ Args:
258
+ price: 基準値(通常は前営業日の終値)
259
+ expansion: 値幅制限の拡大倍率(1=通常, 2=2倍, 3=3倍...)
260
+
261
+ Returns:
262
+ (upper_limit, lower_limit): ストップ高とストップ安の価格のタプル
263
+
264
+ Raises:
265
+ ValueError: 価格が負の値など無効な場合
266
+ """
267
+ # 東証の値幅制限テーブル: (基準価格の上限, 値幅)
268
+ # 該当する値幅を検索
269
+ for limit_price, width in PRICE_LIMIT_TABLE:
270
+ if price < limit_price:
271
+ adjusted_width = width * expansion
272
+ upper_limit = round(price + adjusted_width, 1)
273
+ lower_limit = round(price - adjusted_width, 1)
274
+ return upper_limit, lower_limit
275
+
276
+ raise ValueError(f"Invalid price: {price}")
277
+
278
+ result_df = df.copy()
279
+
280
+ # 前日終値を基準に値幅制限を計算
281
+ prev_close = result_df['Close'].shift(1)
282
+
283
+ # ストップ高・ストップ安を計算(初日はNone)
284
+ def calculate_limits(price):
285
+ if pd.notna(price):
286
+ return _get_price_limit_range(price)
287
+ return None, None
288
+
289
+ limits = prev_close.apply(calculate_limits)
290
+
291
+ if not 'UpperLimit' in result_df.columns:
292
+ result_df['UpperLimit'] = limits.apply(lambda x: x[0])
293
+
294
+ if not 'LowerLimit' in result_df.columns:
295
+ result_df['LowerLimit'] = limits.apply(lambda x: x[1])
296
+
297
+ return result_df
298
+
299
+
300
+ def _get_yfinance_daily_quotes(code: str, from_: datetime = None, to: datetime = None) -> pd.DataFrame:
301
+ """
302
+ Yahoo Finance APIから直接株価データを取得する
303
+
304
+ Args:
305
+ code (str): 銘柄コード(例: '7203')
306
+ from_ (datetime): データ取得開始日
307
+ to (datetime): データ取得終了日
308
+
309
+ Returns:
310
+ pd.DataFrame: 株価データのDataFrame(失敗時は空のDataFrame)
311
+ """
312
+ try:
313
+ # Yahoo Finance APIのURL(.T は東証を表す)
314
+ url = f"https://query1.finance.yahoo.com/v8/finance/chart/{code}.T"
315
+
316
+ headers = {
317
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
318
+ }
319
+
320
+ # パラメータの設定
321
+ params = {
322
+ 'interval': '1d'
323
+ }
324
+
325
+ # 日付範囲が指定されている場合
326
+ if from_ is not None and to is not None:
327
+ # UNIXタイムスタンプに変換
328
+ params['period1'] = int(from_.timestamp())
329
+ params['period2'] = int(to.timestamp())
330
+ elif from_ is not None:
331
+ params['period1'] = int(from_.timestamp())
332
+ params['period2'] = int(datetime.now().timestamp())
333
+ elif to is not None:
334
+ # 開始日が指定されていない場合、1年前から取得
335
+ params['period1'] = int((to - timedelta(days=365)).timestamp())
336
+ params['period2'] = int(to.timestamp())
337
+ else:
338
+ # 日付範囲が指定されていない場合、デフォルトで1年分
339
+ params['range'] = '1y'
340
+
341
+ logger.debug(f"Yahoo Finance APIからデータ取得中: {code}.T")
342
+ response = requests.get(url, headers=headers, params=params, timeout=10)
343
+
344
+ if response.status_code != 200:
345
+ logger.warning(f"Yahoo Finance APIからデータ取得失敗: HTTPステータス {response.status_code}")
346
+ return pd.DataFrame()
347
+
348
+ data = response.json()
349
+
350
+ # レスポンスの検証
351
+ if 'chart' not in data or 'result' not in data['chart'] or len(data['chart']['result']) == 0:
352
+ logger.warning(f"Yahoo Finance APIからデータが返されませんでした: {code}")
353
+ return pd.DataFrame()
354
+
355
+ result = data['chart']['result'][0]
356
+
357
+ # エラーチェック
358
+ if 'error' in result and result['error'] is not None:
359
+ logger.error(f"Yahoo Finance APIエラー: {result['error']}")
360
+ return pd.DataFrame()
361
+
362
+ # データの抽出
363
+ if 'timestamp' not in result or 'indicators' not in result:
364
+ logger.warning(f"Yahoo Finance APIのレスポンス形式が不正です: {code}")
365
+ return pd.DataFrame()
366
+
367
+ timestamps = result['timestamp']
368
+ quotes = result['indicators']['quote'][0]
369
+
370
+ # DataFrameの作成
371
+ df = pd.DataFrame({
372
+ 'Date': pd.to_datetime(timestamps, unit='s'),
373
+ 'Open': quotes['open'],
374
+ 'High': quotes['high'],
375
+ 'Low': quotes['low'],
376
+ 'Close': quotes['close'],
377
+ 'Volume': quotes['volume']
378
+ })
379
+
380
+ # Dateをインデックスに設定
381
+ df = df.set_index('Date')
382
+
383
+ # 欠損値を含む行を削除
384
+ df = df.dropna()
385
+
386
+ # Adj Close が存在する場合は追加
387
+ if 'adjclose' in result['indicators'] and len(result['indicators']['adjclose']) > 0:
388
+ adj_close = result['indicators']['adjclose'][0]['adjclose']
389
+ df['Adj Close'] = adj_close
390
+ else:
391
+ # Adj Close が存在しない場合はCloseと同じ値を使用
392
+ df['Adj Close'] = df['Close']
393
+
394
+ if df.empty:
395
+ logger.warning(f"Yahoo Finance APIから取得したデータが空でした: {code}")
396
+ else:
397
+ logger.info(f"Yahoo Finance APIからデータ取得成功: {code} ({len(df)}件)")
398
+
399
+ return df
400
+
401
+ except requests.exceptions.RequestException as e:
402
+ logger.error(f"Yahoo Finance APIへのリクエスト中にエラーが発生しました: {e}")
403
+ return pd.DataFrame()
404
+ except (KeyError, IndexError, ValueError) as e:
405
+ logger.error(f"Yahoo Finance APIのレスポンス解析中にエラーが発生しました: {e}")
406
+ return pd.DataFrame()
407
+ except Exception as e:
408
+ logger.error(f"Yahoo Finance APIからのデータ取得中に予期しないエラーが発生しました: {e}")
409
+ return pd.DataFrame()
@@ -0,0 +1,38 @@
1
+ import pandas as pd
2
+
3
+
4
+ # 東証の値幅制限テーブル: (基準価格の上限, 値幅)
5
+ PRICE_LIMIT_TABLE = [
6
+ (100, 30),
7
+ (200, 50),
8
+ (500, 80),
9
+ (700, 100),
10
+ (1000, 150),
11
+ (1500, 300),
12
+ (2000, 400),
13
+ (3000, 500),
14
+ (5000, 700),
15
+ (7000, 1000),
16
+ (10000, 1500),
17
+ (15000, 3000),
18
+ (20000, 4000),
19
+ (30000, 5000),
20
+ (float('inf'), 10000),
21
+ ]
22
+
23
+
24
+ def _Timestamp(value):
25
+ """
26
+ from_/to に与えられる日付入力(str, datetime.date, datetime, pd.Timestamp, None)
27
+ を pandas.Timestamp(もしくは None)に正規化する。
28
+
29
+ 不正な文字列などは ValueError とする。
30
+ """
31
+ if value is None:
32
+ return None
33
+ try:
34
+ ts = pd.to_datetime(value, errors='raise')
35
+ # pandas.Timestamp は strftime を持つため、そのまま返す
36
+ return ts
37
+ except Exception:
38
+ raise ValueError(f"日付パラメータの形式が不正です: {value}")
@@ -0,0 +1,77 @@
1
+ from .lib.jquants import jquants
2
+ from .lib.e_api import e_api
3
+ from .lib.kabusap import kabusap
4
+ from .lib.stooq import stooq_daily_quotes
5
+ from .db_stocks_daily import db_stocks_daily
6
+ from .db_stocks_board import db_stocks_board
7
+ import pandas as pd
8
+ import threading
9
+ from datetime import datetime
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+ logger.setLevel(logging.INFO)
14
+
15
+ class stocks_board:
16
+ """
17
+ 銘柄の板情報を取得するためのクラス
18
+ """
19
+
20
+ def __init__(self):
21
+ self.db = db_stocks_board()
22
+
23
+
24
+ def get_japanese_stock_board_data(self, code = "", date: datetime = None) -> pd.DataFrame:
25
+
26
+ # 銘柄コードの検証
27
+ if not code or not isinstance(code, str) or not code.strip():
28
+ raise ValueError("銘柄コードが指定されていません")
29
+
30
+ # DBファイルの準備(存在しなければFTPからダウンロードを試行)
31
+ self.db.ensure_db_ready(code)
32
+
33
+ # 時間が指定されている場合、指定時刻の板情報を取得
34
+ if date is not None:
35
+ df = self.db.load_stock_board_from_cache(code, date)
36
+ if df is not None and not df.empty:
37
+ return df
38
+ # 時間に指定がある場合、取得できなければエラー
39
+ raise ValueError(f"{date}: 板情報の取得に失敗しました: {code}")
40
+
41
+ # 1) kabuステーションから取得
42
+ if not hasattr(self, 'kabusap'):
43
+ self.kabusap = kabusap()
44
+ if self.kabusap.isEnable:
45
+ df = self.kabusap.get_board(code=code)
46
+ if df is not None and not df.empty:
47
+ # DataFrameをDuckDBに保存
48
+ ## 非同期、遅延を避けるためデーモンスレッドで実行
49
+ threading.Thread(
50
+ target=self.db.save_stock_board, args=(code, df), daemon=True
51
+ ).start()
52
+ return df
53
+
54
+ # 2) 立花証券 e-支店から取得
55
+ if not hasattr(self, 'e_shiten'):
56
+ self.e_shiten = e_api()
57
+ if self.e_shiten.isEnable:
58
+ df = self.e_shiten.get_board(code=code)
59
+ if df is not None and not df.empty:
60
+ # DataFrameをDuckDBに保存
61
+ ## 非同期、遅延を避けるためデーモンスレッドで実行
62
+ threading.Thread(
63
+ target=self.db.save_stock_board, args=(code, df), daemon=True
64
+ ).start()
65
+ return df
66
+
67
+ raise ValueError(f"板情報の取得に失敗しました: {code}")
68
+
69
+
70
+ def get_stock_board(code, date: datetime = None) -> pd.DataFrame:
71
+ """
72
+ 板情報を取得する
73
+ """
74
+ from .stocks_board import stocks_board
75
+ __sb__ = stocks_board()
76
+
77
+ return __sb__.get_japanese_stock_board_data(code=code, date=date)
@@ -0,0 +1,88 @@
1
+ from .lib.jquants import jquants
2
+ from .db_stocks_info import db_stocks_info
3
+ import sys
4
+ import pandas as pd
5
+ import threading
6
+ from datetime import datetime
7
+ import logging
8
+
9
+ logger = logging.getLogger(__name__)
10
+ logger.setLevel(logging.INFO)
11
+
12
+ class stocks_info:
13
+ """
14
+ 銘柄のデータを取得するためのクラス
15
+ """
16
+
17
+ def __init__(self):
18
+ self.db = db_stocks_info()
19
+ self.jq = jquants()
20
+
21
+ def get_japanese_listed_info(self, code = "", date: datetime = None) -> pd.DataFrame:
22
+
23
+ # DBファイルの準備(存在しなければFTPからダウンロードを試行)
24
+ self.db.ensure_db_ready()
25
+
26
+ # 1) J-Quantsから取得
27
+ if self.jq.isEnable:
28
+ df = self.jq.get_listed_info(code=code, date=date)
29
+ # Codeをが4文字にする
30
+ df['Code'] = df['Code'].str[:4]
31
+ if df is not None and not df.empty:
32
+ # DataFrameをcacheフォルダに保存
33
+ ## 非同期、遅延を避けるためデーモンスレッドで実行
34
+ threading.Thread(target=self.db.save_listed_info, args=(df,), daemon=True).start()
35
+ return df
36
+
37
+ # 2) cacheフォルダから取得
38
+ df = self.db.load_listed_info_from_cache(code, date)
39
+ if df.empty:
40
+ # 空のDataFrameの場合は次のデータソースを試す
41
+ pass
42
+ else:
43
+ return df
44
+
45
+ raise ValueError(f"日本株式上場銘柄一覧の取得に失敗しました: {self.jq.isEnable}")
46
+
47
+ def get_company_name(self, code: str):
48
+ """
49
+ 銘柄コードを指定して銘柄名称を取得する
50
+ """
51
+ if not self.jq.isEnable:
52
+ return str(code)
53
+
54
+ title = None
55
+ try:
56
+ # 銘柄コードを正規化(4桁の場合はそのまま、5桁の場合はそのまま)
57
+ code_for_lookup = str(code).strip()
58
+ # .JPなどのサフィックスを除去
59
+ if '.' in code_for_lookup:
60
+ code_for_lookup = code_for_lookup.split('.')[0]
61
+
62
+ # 銘柄情報を取得
63
+ df_info = self.jq.get_listed_info(code=code_for_lookup)
64
+
65
+ # 銘柄名称を取得(CompanyNameカラムから)
66
+ if not df_info.empty and 'CompanyName' in df_info.columns:
67
+ company_name = df_info.iloc[0]['CompanyName']
68
+ if pd.notna(company_name) and company_name:
69
+ title = str(company_name)
70
+ except Exception as e:
71
+ # エラーが発生してもチャートの表示は継続(タイトルなしで表示)
72
+ print(f"警告: 銘柄名称の取得に失敗しました: {e}", file=sys.stderr)
73
+
74
+ # タイトルが取得できなかった場合は、銘柄コードをフォールバックとして使用
75
+ if title is None:
76
+ title = str(code)
77
+
78
+ return title
79
+
80
+
81
+ def get_stock_info(code="", date: datetime = None) -> pd.DataFrame:
82
+ """
83
+ 銘柄の情報を取得する
84
+ """
85
+ from .stocks_info import stocks_info
86
+ __si__ = stocks_info()
87
+
88
+ return __si__.get_japanese_listed_info(code=code, date=date)