はじめに
システムでデイトレするためには株価の変化をリアルタイムで欲しいですよね?
そんな時に使えるのが 立花証券e支店 API の EVENT I/F です。
EVENT I/F にはHTTP Protocol版とWebSocket Protocol版がありますが、ここではWebSocket版の話です。
今は eスマート証券(旧カブコム証券) の kabuSTATION API を使ったプログラムを運用してますが、ちょっと立花証券e支店 API は、どんなものかと思って開発中です。
で、立花証券e支店 API の情報が少ないと思ったので投稿してみます。
一応、公式サンプルとかはあるのですが…
ちなみに、立花証券e支店APIは先日(2025-09-27) v4r8がリリースされて POST対応 されました。
これでやっと違和感なく使える感じになりましたね。
WebSocketApp
ここで紹介するのは websocket-client ライブラリの WebSocketApp を使用したものです。
WebSocketAppは非常にシンプルなI/Fで気に入ってます。
非同期I/O asyncio には対応していないのですが、今回は使わない予定です。
というのも、前述のいま運用しているプログラムをGeminiさんに非同期I/O化して貰ったのですが、逆に遅くなってしまいました。
自分のデイトレプログラムはリアルタイムで株価を受け取ってOHLCや複数の移動平均値を計算してるのですが、asyncioが苦手な「CPUバウンドな処理」になるようです。
ということで、事前にライブラリをインストールしておきましょう。
$ uv add websocket-client
$ uv add requests
又は
$ pip install websocket-client
$ pip install requests
※uv を使いだすと、もう便利で戻れませんね。
WebSocket Client サンプル
必要最低限でエラー処理などしていません。
from urllib.parse import urlencode
import websocket
class TeWebSocketClient:
def __init__(self, ws_url: str):
self.ws_url = ws_url
self.ws = None
self._is_connected = False
def _on_message(self, ws, message):
print(f"Received: {message}")
def _on_error(self, ws, error):
print(f"Error: {error}")
def _on_close(self, ws, close_status_code, close_msg):
print("### closed ###")
self._is_connected = False
def _on_open(self, ws):
print("Opened connection")
self._is_connected = True
def connect(self):
# EVENT I/F へのパラメーター
payload = {}
payload["p_rid"] = "22"
payload["p_board_no"] = "1000"
payload["p_eno"] = "0"
payload["p_evt_cmd"] = "ST,KP,FD,EC,NS,SS,US"
# 以下、本来は動的に生成
payload["p_issue_code"] = "7201,7202,7203"
payload["p_gyou_no"] = "1,2,3"
payload["p_mkt_code"] = "00,00,00"
# EVENT I/F はPOST対応していない(v4r8時点)
# パラメータ付URL(カンマ(,)はエンコードしない)
full_url = f"{self.ws_url}?{urlencode(payload, safe=',')}"
self.ws = websocket.WebSocketApp(
full_url,
on_open=self._on_open,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
)
# Run in a separate thread to not block the main thread
import threading
self.ws_thread = threading.Thread(target=self.ws.run_forever)
self.ws_thread.daemon = (
True # Allow the main program to exit even if the thread is still running
)
self.ws_thread.start()
# Wait a bit for the connection to establish
import time
time.sleep(1)
return self._is_connected
def disconnect(self):
if self.ws:
self.ws.close()
self.ws = None
self._is_connected = False
def is_connected(self):
return self._is_connected
def join(self):
if self.ws_thread:
self.ws_thread.join()
def send_message(self, message: str):
if self._is_connected and self.ws:
self.ws.send(message)
else:
print("WebSocket is not connected.")
return False
return True
パラメーターに渡す ws_url はログインすると発行される仮想URLの一つなので、先にログインする必要があります。
ログイン用のクラスがこちら!
import threading
import typing
from datetime import datetime
import requests
class TeApiClient:
def __init__(self):
self.session = requests.Session()
self._lock = threading.Lock()
self.request_counter: int = 0
# ログイン情報
self.session_info: dict = {}
def _get_next_p_no(self) -> str:
"""
APIリクエストごとにインクリメントされるp_no(リクエスト番号)を生成します。
"""
self.request_counter += 1
return str(self.request_counter)
def _get_current_datetime(self) -> str:
"""
p_sd_date用の日時文字列(YYYY.MM.DD-HH:MI:SS.ms)を生成します。
"""
# APIの仕様に合わせ、マイクロ秒を示す末尾3桁を削除しミリ秒までの精度にします。
return datetime.now().strftime("%Y.%m.%d-%H:%M:%S.%f")[:-3]
def _call_api(
self,
url: str,
payload: dict,
method: str = "POST",
headers: dict[str, str] = {},
) -> dict:
"""
APIサーバーへリクエストを送信し、結果を取得する共通メソッド。
APIをコールし、取得したJSONの辞書オブジェクトを返します。
このメソッド内で、リクエスト共通のパラメータが付与されます。
Args:
url (str): エンドポイントURL。
payload (dict): 送信パラメータ。
method (str): "GET" または "POST"。
headers (dict[str, str]): HTTPヘッダー。
Raises:
requests.exceptions.RequestException: APIサーバーへ接続出来なかった場合。
Exception: その他の例外発生時。
Returns:
dict: APIから取得したJSONを辞書へ変換したオブジェクト。
"""
dict_result: dict = {}
with self._lock:
try:
# 共通パラメーターの付与
payload["p_no"] = self._get_next_p_no()
payload["p_sd_date"] = self._get_current_datetime()
payload["sJsonOfmt"] = "4"
response = self.session.request(
method=method,
url=url,
headers=headers,
json=payload,
timeout=20,
)
response.raise_for_status()
response.encoding = "cp932"
if "application/json" in response.headers.get("Content-Type", ""):
dict_result = response.json()
return dict_result
else:
print(response.text)
return {"p_errno": "9999", "p_err": "No JSON"}
except requests.exceptions.RequestException as exp:
print(exp)
if exp.response is not None:
print(
f"CODE: {exp.response.status_code}, REASON: {exp.response.reason}"
)
raise exp
return dict_result
def get_info(self, key: str, default: typing.Any = None) -> typing.Any:
"""
ログイン時に取得したセッション情報から、指定したキーの値を取得します。
"""
return self.session_info.get(key, default)
def login(self, url: str, user_id: str, password: str) -> bool:
payload = {
"sCLMID": "CLMAuthLoginRequest",
"sUserId": user_id,
"sPassword": password,
}
results = self._call_api(url, payload)
print(results)
self.session_info = results
if self.get_info("sUrlRequest") is None:
print("契約締結前書面が未読です。")
return False
return True
def logout(self) -> bool:
payload = {
"sCLMID": "CLMAuthLogoutRequest",
}
url = self.get_info("sUrlRequest")
results = self._call_api(url, payload)
print(results)
self.session_info = {}
return True
メインモジュールは、こちら!
import time
from te_api_client import TeApiClient
from te_websocket_client import TeWebSocketClient
def main():
# 立花証券e支店のデモ環境
url = "https://demo-kabuka.e-shiten.jp/e_api_v4r8/auth/"
# 自分のID/PW
user_id = "XXXX"
password = "XXXX"
api_client = TeApiClient()
# ログイン
result = api_client.login(url, user_id, password)
if result is False:
print("ログイン失敗")
return
# EVENT I/F WebSocket版のURL
ws_url = api_client.get_info("sUrlEventWebSocket")
print(ws_url)
ws_client = TeWebSocketClient(ws_url)
# WebSocket接続(別スレッドが起動する)
ws_client.connect()
while ws_client.is_connected():
try:
time.sleep(1)
except KeyboardInterrupt:
# Ctrl+C で終了
print("Interrupted")
break
ws_client.disconnect()
ws_client.join()
# ログアウト
api_client.logout()
if __name__ == "__main__":
main()
実行すると以下のような出力が得られます。
(.venv) PS > python main.py
{'p_sd_date': '2025.10.23-23:26:20.602', 'p_no': '1', 'p_rv_date': '2025.10.23-23:26:20.409', 'p_errno': '0', 'p_err': '', 'sCLMID': 'CLMAuthLoginAck', 'sResultCode': '0', 'sResultText': '', 'sZyoutoekiKazeiC': '1', 'sSecondPasswordOmit': '0', 'sLastLoginDate': '20251023230250', 'sSogoKouzaKubun': '1', 'sHogoAdukariKouzaKubun': '1', 'sFurikaeKouzaKubun': '1', 'sGaikokuKouzaKubun': '1', 'sMRFKouzaKubun': '0', 'sTokuteiKouzaKubunGenbutu': '1', 'sTokuteiKouzaKubunSinyou': '1', 'sTokuteiKouzaKubunTousin': '0', 'sTokuteiHaitouKouzaKubun': '1', 'sTokuteiKanriKouzaKubun': '0', 'sSinyouKouzaKubun': '1', 'sSakopKouzaKubun': '0', 'sMMFKouzaKubun': '0', 'sTyukokufKouzaKubun': '0', 'sKawaseKouzaKubun': '0', 'sHikazeiKouzaKubun': '1', 'sKinsyouhouMidokuFlg': '0', 'sUrlRequest': 'https://demo-kabuka.e-shiten.jp/e_api_v4r8/request/NDA5MjAyNjIzMjMxMC0xMTYtNTM5MzA=/', 'sUrlMaster': 'https://demo-kabuka.e-shiten.jp/e_api_v4r8/master/NDA5MjAyNjIzMjMxMC0xMTYtNTM5MzA=/', 'sUrlPrice': 'https://demo-kabuka.e-shiten.jp/e_api_v4r8/price/NDA5MjAyNjIzMjMxMC0xMTYtNTM5MzA=/', 'sUrlEvent': 'https://demo-kabuka.e-shiten.jp/e_api_v4r8/event/NDA5MjAyNjIzMjMxMC0xMTYtNTM5MzA=/', 'sUrlEventWebSocket': 'wss://demo-kabuka.e-shiten.jp/e_api_v4r8/event_ws/NDA5MjAyNjIzMjMxMC0xMTYtNTM5MzA=/', 'sUpdateInformWebDocument': '20241103', 'sUpdateInformAPISpecFunction': '20241103'}
wss://demo-kabuka.e-shiten.jp/e_api_v4r8/event_ws/NDA5MjAyNjIzMjMxMC0xMTYtNTM5MzA=/
Opened connection
Received: p_no1p_date2025.10.23-23:26:20.693p_cmdKP
Received: p_no2p_date2025.10.23-23:26:20.696p_cmdFDp_1_AV94100p_1_BV100p_1_DHF0000p_1_DHP377.7p_1_DHP:T14:33p_1_DJ6635432420p_1_DLF0000p_1_DLP370.8p_1_DLP:T10:00p_1_DOP373.8p_1_DOP:T09:00p_1_DPG0057p_1_DPP377.6p_1_DPP:T15:30p_1_DV17673900p_1_DYRP-0.02p_1_DYWP-0.1p_1_GAP1377.6p_1_GAP10378.5p_1_GAP2377.7p_1_GAP3377.8p_1_GAP4377.9p_1_GAP5378.0p_1_GAP6378.1p_1_GAP7378.2p_1_GAP8378.3p_1_GAP9378.4p_1_GAV194100p_1_GAV1015700p_1_GAV211200p_1_GAV37300p_1_GAV412200p_1_GAV591700p_1_GAV632100p_1_GAV71300p_1_GAV8100p_1_GAV910400p_1_GBP1377.4p_1_GBP10376.2p_1_GBP2377.2p_1_GBP3377.0p_1_GBP4376.9p_1_GBP5376.8p_1_GBP6376.6p_1_GBP7376.5p_1_GBP8376.4p_1_GBP9376.3p_1_GBV1100p_1_GBV10100p_1_GBV2400p_1_GBV37000p_1_GBV4600p_1_GBV5300p_1_GBV62000p_1_GBV76200p_1_GBV856500p_1_GBV910000p_1_LISSCCDFD7B2D1p_1_PRP377.7p_1_QAP377.6p_1_QAS0101p_1_QBP377.4p_1_QBS0101p_1_QOV7279500p_1_QUV5788800p_1_VWAP375.4368p_2_AV200p_2_BV500p_2_DHF0000p_2_DHP1939.0p_2_DHP:T14:15p_2_DJ3731461100p_2_DLF0000p_2_DLP1915.5p_2_DLP:T09:42p_2_DOP1928.0p_2_DOP:T09:00p_2_DPG0058p_2_DPP1931.0p_2_DPP:T15:30p_2_DV1932500p_2_DYRP-0.02p_2_DYWP-0.5p_2_GAP11936.0p_2_GAP101940.5p_2_GAP21936.5p_2_GAP31937.0p_2_GAP41937.5p_2_GAP51938.0p_2_GAP61938.5p_2_GAP71939.0p_2_GAP81939.5p_2_GAP91940.0p_2_GAV1200p_2_GAV10400p_2_GAV2100p_2_GAV3700p_2_GAV41100p_2_GAV5200p_2_GAV63000p_2_GAV73200p_2_GAV813600p_2_GAV918100p_2_GBP11930.5p_2_GBP101926.0p_2_GBP21930.0p_2_GBP31929.5p_2_GBP41929.0p_2_GBP51928.5p_2_GBP61928.0p_2_GBP71927.5p_2_GBP81927.0p_2_GBP91926.5p_2_GBV1500p_2_GBV10400p_2_GBV214400p_2_GBV3400p_2_GBV47600p_2_GBV5400p_2_GBV6500p_2_GBV7400p_2_GBV89200p_2_GBV9400p_2_LISSCCDFD7B2D1p_2_PRP1931.5p_2_QAP1936.0p_2_QAS0101p_2_QBP1930.5p_2_QBS0101p_2_QOV693000p_2_QUV462200p_2_VWAP1930.8984p_3_AV19200p_3_BV1400p_3_DHF0000p_3_DHP3112p_3_DHP:T09:00p_3_DJ55764856000p_3_DLF0000p_3_DLP3057p_3_DLP:T09:39p_3_DOP3100p_3_DOP:T09:00p_3_DPG0057p_3_DPP3091p_3_DPP:T15:30p_3_DV18085400p_3_DYRP-0.41p_3_DYWP-13p_3_GAP13092p_3_GAP103101p_3_GAP23093p_3_GAP33094p_3_GAP43095p_3_GAP53096p_3_GAP63097p_3_GAP73098p_3_GAP83099p_3_GAP93100p_3_GAV119200p_3_GAV1010300p_3_GAV265100p_3_GAV310100p_3_GAV452200p_3_GAV5101600p_3_GAV637000p_3_GAV7115700p_3_GAV821700p_3_GAV9333700p_3_GBP13090p_3_GBP103081p_3_GBP23089p_3_GBP33088p_3_GBP43087p_3_GBP53086p_3_GBP63085p_3_GBP73084p_3_GBP83083p_3_GBP93082p_3_GBV11400p_3_GBV1047500p_3_GBV2900p_3_GBV36100p_3_GBV4200p_3_GBV52300p_3_GBV672900p_3_GBV76900p_3_GBV843500p_3_GBV927600p_3_LISSCCDFD7B2D1p_3_PRP3104p_3_QAP3092p_3_QAS0101p_3_QBP3090p_3_QBS0101p_3_QOV6718500p_3_QUV3647100p_3_VWAP3083.4184
Received: p_no3p_date2025.10.23-23:26:20.812p_cmdSSp_PVMSGSVp_ENO44p_ALT0p_CT20251023081012p_LK1p_SS1
Received: p_no4p_date2025.10.23-23:26:20.812p_cmdUSp_PVMSGSVp_ENO56p_ALT0p_CT20251023084015p_MC00p_GSCDp_SHSBp_UC01p_UU0101p_EDK0p_US100
Received: p_no5p_date2025.10.23-23:26:20.812p_cmdUSp_PVMSGSVp_ENO158p_ALT0p_CT20251023111012p_MC00p_GSCDp_SHSBp_UC01p_UU0101p_EDK0p_US140
Received: p_no6p_date2025.10.23-23:26:20.812p_cmdUSp_PVMSGSVp_ENO167p_ALT0p_CT20251023111212p_MC00p_GSCDp_SHSBp_UC01p_UU0101p_EDK0p_US160
Received: p_no7p_date2025.10.23-23:26:20.921p_cmdUSp_PVMSGSVp_ENO180p_ALT0p_CT20251023114512p_MC00p_GSCDp_SHSBp_UC01p_UU0101p_EDK0p_US200
Received: p_no8p_date2025.10.23-23:26:20.921p_cmdUSp_PVMSGSVp_ENO272p_ALT0p_CT20251023144011p_MC00p_GSCDp_SHSBp_UC01p_UU0101p_EDK0p_US260
Received: p_no9p_date2025.10.23-23:26:20.921p_cmdUSp_PVMSGSVp_ENO284p_ALT0p_CT20251023144212p_MC00p_GSCDp_SHSBp_UC01p_UU0101p_EDK0p_US280
Received: p_no10p_date2025.10.23-23:26:20.921p_cmdUSp_PVMSGSVp_ENO299p_ALT0p_CT20251023151004p_MC00p_GSCDp_SHSBp_UC01p_UU0101p_EDK0p_US200
Received: p_no11p_date2025.10.23-23:26:25.013p_cmdKP
Received: p_no12p_date2025.10.23-23:26:30.035p_cmdKP
Received: p_no13p_date2025.10.23-23:26:35.058p_cmdKP
Received: p_no14p_date2025.10.23-23:26:40.081p_cmdKP
Received: p_no15p_date2025.10.23-23:26:45.003p_cmdKP
Received: p_no16p_date2025.10.23-23:26:50.026p_cmdKP
Interrupted
### closed ###
{'p_sd_date': '2025.10.23-23:26:56.046', 'p_no': '2', 'p_rv_date': '2025.10.23-23:26:55.993', 'p_errno': '0', 'p_err': '', 'sCLMID': 'CLMAuthLogoutAck', 'sResultCode': '0', 'sResultText': ''}
(.venv) >
最初の一行目はログインした時に得られるログイン情報です。
仮想URLはログアウトすると無効になるので公開しても問題なしということです。
Received: に続く文字列が受信したメッセージです。
これは特殊文字(^A, ^B, ^C)で区切られているので、これを解析して辞書型などへ変換します。
もう少しアプリケーションに近い形のソースコードやメッセージ解析について解説していますので、良ければ参考にしてください。
役に立ったら「いいね」お願いします。