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,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)
|