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.
- BackcastPro/__init__.py +28 -0
- BackcastPro/_broker.py +430 -0
- BackcastPro/_stats.py +177 -0
- BackcastPro/api/__init__.py +4 -0
- BackcastPro/api/board.py +130 -0
- BackcastPro/api/chart.py +527 -0
- BackcastPro/api/db_manager.py +283 -0
- BackcastPro/api/db_stocks_board.py +428 -0
- BackcastPro/api/db_stocks_daily.py +507 -0
- BackcastPro/api/db_stocks_info.py +260 -0
- BackcastPro/api/lib/__init__.py +4 -0
- BackcastPro/api/lib/e_api.py +588 -0
- BackcastPro/api/lib/jquants.py +384 -0
- BackcastPro/api/lib/kabusap.py +222 -0
- BackcastPro/api/lib/stooq.py +409 -0
- BackcastPro/api/lib/util.py +38 -0
- BackcastPro/api/stocks_board.py +77 -0
- BackcastPro/api/stocks_info.py +88 -0
- BackcastPro/api/stocks_price.py +131 -0
- BackcastPro/backtest.py +594 -0
- BackcastPro/order.py +161 -0
- BackcastPro/position.py +60 -0
- BackcastPro/trade.py +227 -0
- backcastpro-0.3.4.dist-info/METADATA +112 -0
- backcastpro-0.3.4.dist-info/RECORD +26 -0
- backcastpro-0.3.4.dist-info/WHEEL +4 -0
|
@@ -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)
|