IB証券(Interactive Brokers)のAPIを試してみたので、その情報を共有しようと思います。
私はプログラミングは素人ですが、ネット上に(日本語の)情報が少ないので情報共有させて頂きます。
ここで紹介するAPIはTWS APIです。TWSまたはIBゲートウェイを介してデータにアクセスします。TWS APIは、TWSの機能を自動化するものであり、TWSで利用できない機能(データ)はAPIでも利用できません。
ただし、TWS APIを直接触るのはハードルが高そうだったので、他サイト様でも良く紹介されているib_insyncというPythonライブラリを使用しました。
Python自体、私は詳しくないため勉強しながらでしたが、それでもコードが簡単に書けるのでお勧めできると思います。
この記事では注文は扱わず、株価データのリクエストのみ扱います。
目次
導入
必要なもの
・Python
・TWSまたはIBゲートウェイ
※ib_insyncを使用する場合、TWS APIソフトウェアは必要ありません。
また、TWS APIのドキュメントおよびib_insyncのドキュメントを必要に応じて参照します。私の記事では適宜、これらのドキュメントの参考箇所へのナビゲートを行っています。英語ですが、Google翻訳やDeepL翻訳、ChatGPTでの翻訳などが役立ちます。
Pythonの準備
これから先のPythonの説明は、Windows版での説明になります。
Python公式サイトから最新版をダウンロードします。インストール時に環境変数にPathを通すか聞かれますが、通さなくても問題ないようです。その場合、コンソールからpython
ではなくpy
コマンドでpythonを呼び出すことができます。
コマンドプロンプトまたはPowerShell上で次の操作を行います。
py -m pip install --upgrade pip
でpipをアップグレードします。py -m pip install ib_insync
でib_insyncをインストールします。
また、私の環境ではib_insyncを使用中にタイムゾーン関係のエラー(Asia/Tokyoが見つからない)が起きたため、以下のようにtzdataをインストールすることで解決しました。py -m pip install tzdata
これでひとまず最低限の準備が整いました。必要なライブラリは適宜インストールします。
開発環境はとりあえずPython付属のIDLEが使えますが、適宜、他のものを使用してください。IDLEの対話モード(一行ずつ実行)では、非同期コード(後述)はうまく動かないようですが、スクリプト(pyファイル)に保存してから実行すると動きます。
TWSまたはIBゲートウェイの準備
APIから見るとどちらでも同じであると書かれています。IBゲートウェイのほうが使用メモリが少なく軽量ですが、TWSであれば既にインストールされていることと思います。
初めはTWSが良いかもしれません。TWSの中で、APIで株価取得したい銘柄の情報を確認できるので便利です。
どちらについても、手動ログインが必要(認証が必要)です。
TWSの場合、設定でAPIを有効にする必要があります。
「API」→「Settings」で、「Enable ActiveX and Socket Clients」にチェックを入れます。
TWS・IBゲートウェイ共通:
「Read-Only API」は、APIで注文を出す場合はチェックを外します。
「Socket port」の番号を後で用います。任意に設定することもできます。
詳しくは以下を参照ください。
https://interactivebrokers.github.io/tws-api/initial_setup.html
ib_insyncの基本
各種リクエストには基本的にib_insyncのIBクラスのメソッドを使用します。
ここでは、マーケットデータを取得する主な方法を紹介しますが、これ以外にも様々なメソッドがあるため、詳しくはIBクラスの項目を参照ください。
IBクラスには、ブロッキング(Blocking)メソッドと非同期メソッドが存在します。Asyncと末尾に付く名称のメソッドはすべて非同期です。例えばreqHistoricalDataメソッドはブロッキングメソッドですが、reqHistoricalDataAsyncという非同期メソッドも存在します。
ブロッキングメソッドは結果が返されるまで待ち、次の処理に進みます。大量のリクエストには時間がかかる可能性がありますが、簡単なコードで実行できます。
非同期メソッドは結果が返されるまで待たずに次の処理に進むため、短時間に大量のリクエストに向いています。
またUtilitiesには、データ処理を便利にするためのメソッドが幾つかあります。
接続
まず以下のコードを実行します。これ以降提示するコードは、このコードの実行が前提です。
from ib_insync import *
# util.startLoop() # Jupyter notebookを使用する場合はコメントアウトを解除します
ib = IB()
ib.connect(host='127.0.0.1', port=7497, clientId=1, timeout=4, readonly=True)
# portに、Socket portの番号を入れます。デフォルトはTWS: 7497、IBゲートウェイ: 4001
# clientIdには任意の番号を入れます。
# readonlyは、注文を出す場合はFalseまたは省略してください。
接続を切る場合は、ib.disconnect()
コントラクトの指定
Contractクラスで、マーケットデータを取得する対象の商品(コントラクト)を指定します。
例
contract = Stock('1301', 'TSEJ', 'JPY')
または、以下の形でも指定できます。contract = Contract(secType='STK', symbol='1301', exchange='TSEJ', currency='JPY')
exchangeに'SMART'も指定できます。
詳しくはContractクラスを参照ください。
TWS APIのドキュメントも参考にできます。
Financial Instruments (Contracts) - TWS API
商品のシンボルや取引所のコードを確認するには、TWSを利用するか、IB証券の商品検索ページから検索するか、IB.reqMatchingSymbols()を利用するかします。
IB.reqMatchingSymbols()では、シンボルの初めの数文字または完全なシンボルを指定して、該当するコントラクトの情報を取得します。
例ib.reqMatchingSymbols('1301')
リアルタイム(ストリーミング)マーケットデータ
リアルタイム(ストリーミング)マーケットデータを取得するには、当該マーケットデータの購読が必要です。購読が無い場合、遅延データであれば利用できる可能性があります。TWSで表示されるデータを確認してみてください。
データタイプの指定
次項で解説するIB.reqMktData()でリアルタイムマーケットデータをリクエストする前に、IB.reqMarketDataType()で以下のデータタイプを指定することができます。
- Live (1)
- リアルタイムデータ。市場が閉まっている時、BidとAskは-1を返します。
- Frozen (2)
- 市場が閉まっている時、BidとAskは市場の最後の値(終値)を返します。リアルタイムデータが利用できる時はリアルタイムデータを返します。
- Delayed (3)
- リアルタイムデータの購読が無い場合、遅延データを返します。リアルタイムデータの購読がある場合はリアルタイムデータを返します。
- Delayed Frozen (4)
- リアルタイムデータの購読が無い場合、遅延のFrozenデータを返します。
例ib.reqMarketDataType(2)
詳しくは以下を参照ください。
Market Data Types - TWS API
リアルタイムマーケットデータのリクエスト
IB.reqMktData()で、現在の株価情報をTickerとして取得できます。非同期メソッドであり、結果が返るまで少し待つ必要があります。Tickerはデフォルトでリアルタイムに更新されます。
例
ticker = ib.reqMktData(contract, genericTickList='', snapshot=False, regulatorySnapshot=False)
ib.sleep(1)
print(ticker)
print(ticker.bid) #Bidを表示
genericTickListには、取得するTickタイプを指定します。何も指定しない場合、デフォルトのTickタイプが返されます。
snapshotがTrueの場合、その時点での一度限りの株価データを取得します(スナップショットデータの取得)。
regulatorySnapshotがTrueの場合、米国の株式およびオプションについて、マーケットデータの購読が無い場合でもスナップショット用の料金を支払うことでスナップショットデータをリクエストします。
スナップショットでは、リクエストから11秒後に結果が返されます。また、Tickタイプはデフォルトのものしか利用できません。
IB.reqMktData()によって取得したTickerは、プロパティ(リンク先参照)を指定することでそれぞれのデータにアクセスできます。
ticker.bid
ticker.bidSize
IB.ticker()でも、IB.reqMktData()によって取得したTicker(株価情報)を取り出すことができます。
ib.ticker(contract)
IB.tickers()では、IB.reqMktData()によって取得された全てのコントラクトのTicker(株価情報)をTicker配列として取り出します。
ib.tickers()
ib_insyncには、IB.reqTickers()というメソッドが用意されています。これは、複数のコントラクトに対してスナップショットデータのリクエストを同時に行うものです。内部的にはreqMktData(snapshot=True)をループさせるような感じになっており、全ての結果が得られるまで待機し、Ticker配列を返します。
例
contracts = [Stock('1301', 'TSEJ', 'JPY'), Stock('1305', 'TSEJ', 'JPY'), Stock('1306', 'TSEJ', 'JPY')]
ib.reqTickers(*contracts)
スナップショットデータのリクエストには11秒間かかりますが、reqTickers()によって複数コントラクトのリクエストをした場合でも、原則、全体で11秒しかかかりません。ただし後述しますが秒間50回のリクエスト数制限があります。
リアルタイム(ストリーミング)マーケットデータは、デフォルトでは同時に100個まで受信できます(Market Data Lines - TWS API)。100を超える商品のデータを受信したい場合は、IB.cancelMktData()を使用して、現在受信している商品をキャンセルする必要があります。
ib.cancelMktData(contract)
スナップショットデータのリクエストに関しては、100個の制限はあまり意識しなくても良いかもしれません。TWSでは、同時に多数のスナップショットリクエストをした場合(reqTickers()で多数のコントラクトを指定した場合など)に警告が表示されますが、特に問題なく動作が継続します。IBゲートウェイでは特に警告は表示されません。
株価情報が更新される度に、指定の処理を行う
前項のIB.reqMktData()で株価情報をストリーミングし、その情報の更新があった時に任意の処理を行う方法について解説します。
IB.pendingTickersEvent(IBクラスのEventsを参照)にイベントハンドラーを追加します。
このイベントは、ストリーミングしているTickerの更新がある度に当該Tickerをリストとして出力します。
具体的にどのような形式で出力されるかですが、まず例としてTickerは以下のような出力になります。見やすいように整形しています。
Ticker(
contract=Stock(symbol='4714', exchange='JPNNEXT', currency='JPY'),
time=datetime.datetime(2024, 4, 9, 9, 3, 54, 829809, tzinfo=datetime.timezone.utc),
minTick=0.1,
bid=304.1,
bidSize=2500.0,
ask=304.2,
askSize=100.0,
last=304.2,
lastSize=100.0,
prevBidSize=2900.0,
prevLast=304.1,
prevLastSize=400.0,
volume=897300.0,
high=310.5,
low=211.0,
close=303.0,
ticks=[
TickData(
time=datetime.datetime(2024, 4, 9, 9, 3, 54, 829809, tzinfo=datetime.timezone.utc),
tickType=4,
price=304.2,
size=100.0
),
TickData(
time=datetime.datetime(2024, 4, 9, 9, 3, 54, 829809, tzinfo=datetime.timezone.utc),
tickType=5,
price=304.2,
size=100.0
)
]
)
この中の、ticks(Ticker.ticks)にTickDataがリストとして入っています。このTickDataが、更新された情報を格納したものになります。
TickDataのtickTypeは、Tickタイプに記載された番号となります。例えば、tickType=4であればLast Priceを意味します。つまりtickTypeが4のTickDataがある場合、約定値が更新されたことを意味し、その値がpriceに格納されています。
tickTypeが5の場合はLast Sizeを意味するため、最新の取引の出来高の更新があることを意味し、その出来高がsizeに格納されています。
ticks以外のプロパティは単に、更新されたもの・されていないもの含め現在の情報を表しています。
IB.pendingTickersEventに、処理を行うための関数(イベントハンドラー)を追加することで、株価情報の更新がある度にTickerリストがイベントハンドラーに渡されて実行されます。
次にコードサンプルですが、約定値の更新がある度に各種情報(時間、シンボル、価格)を出力するコードを書いてみます。
contract = Stock('4714', 'JPNNEXT', 'JPY')
#イベントハンドラーを定義
def on_price_change(tickers):
for ticker in tickers:
for tick in ticker.ticks:
if tick.tickType != 4:
continue
print(f"{ticker.time.strftime('%H:%M:%S')} "\
f"Symbol:{ticker.contract.symbol} Price:{tick.price}")
ib.reqMktData(contract)
ib.pendingTickersEvent += on_price_change #イベントにイベントハンドラーを追加し、処理を開始
ib.sleep(60) #処理の終了まで任意の時間待機
ib.pendingTickersEvent -= on_price_change #イベントハンドラーを削除し、処理を終了
その他
IB.reqTickByTickData()で歩み値(Time&Sales)を取得できます。
IB.reqMktDepth()で板情報を取得できます。
リアルタイムマーケットデータに関して、詳しくは以下を参照ください。
Streaming Market Data - TWS API
ヒストリカルマーケットデータ
ヒストリカルデータを利用する場合にも、リアルタイムマーケットデータの購読が必要です。
ヒストリカルデータは、バーデータ(四本値)、ヒストグラム(価格分布)、歩み値(Time&Sales)が利用できます。ここでは、バーデータの取得について解説します。
バーデータ(四本値)のリクエスト
IB.reqHistoricalData()を用います。これはブロッキングメソッドです。
bars = ib.reqHistoricalData(
contract,
endDateTime='',
durationStr='30 D',
barSizeSetting='1 hour',
whatToShow='MIDPOINT',
useRTH=True
)
df = util.df(bars)
print(df)
上記はAPIドキュメントに載っているサンプルからの抜粋です。
IB.reqHistoricalData()のパラメーターについては、endDateTimeでデータの終了時点(''で現在を指定)、durationStrでendDateTimeから遡る期間、barSizeSettingで足の長さ(5分足や日足など)、whatToShowでデータタイプ(Bis/Ask、仲値、約定価格など)、useRTHで通常取引時間のデータのみ使用するかどうかを指定します。指定方法など詳しくはIB.reqHistoricalData()やHistorical Bar Data - TWS APIを参照してください。
util.df()で、pandas DataDrameを用いてデータを整形して表示させています(pandasのインストールが必要)。
一つのバー(足)は、一般的なチャートと同じく、取引セッションを跨ぎません。
バーの開始時点は時間足では○時ちょうど、分足では○分ちょうどになります。
取引セッションの開始が例えば○時30分の時に、時間足を指定すると、取引セッション開始後30分のバーが生成され、その後に時間足のバーが生成されます。
IB.reqHistoricalData()の戻り値はBarDataListであるため、まずこの配列からBarDataを取り出し、そこから各種プロパティ(open, high, low, close, volumeなど)にアクセスすることができます。
bars[-1].close #最新のバーの終値
非同期メソッドとしてIB.reqHistoricalDataAsync()もあります。
ただしヒストリカルデータについて同時にリクエストできるのは50までに制限されています。
加えて、30秒足以下のヒストリカルバーデータについては、以下の制限があります。
- 15秒以内に同一リクエストを行う。
- 同じコントラクト、取引所、Tickタイプのリクエストを2秒以内に6回以上行う。
- 10分間以内に 60件を超えるリクエストを行う。
ヒストリカルデータのリクエストは処理が重いようで、例え少量のデータのリクエストでも時間がかかることがあります。非同期メソッドのIB.reqHistoricalDataAsync()で複数コントラクトを同時にリクエストすると、タイムアウトで結果が返ってこないものが出てきたので、パラメーターでタイムアウトの時間を延ばすか、同時リクエストの数を減らすか等する必要があると思います(デフォルトのタイムアウト時間では30個くらいが上限?)。普通にブロッキングメソッドのIB.reqHistoricalData()をループさせるほうが遅いですが確実にデータを取得できました。
一応、IB.reqHistoricalDataAsync()を用いて複数コントラクトを同時リクエストするサンプルコードを貼っておきます。非同期コードの参考にしてください。と言っても私は素人なので自信はありませんが一応動きました。
import asyncio
contracts = [Stock('1301', 'TSEJ', 'JPY'), Stock('1305', 'TSEJ', 'JPY'), Stock('1306', 'TSEJ', 'JPY')]
async def fetch_historical_bars_list(contracts):
tasks = [ib.reqHistoricalDataAsync(
contract,
endDateTime='',
durationStr='30 D',
barSizeSetting='1 day',
whatToShow='TRADES',
useRTH=True
) for contract in contracts]
results = await asyncio.gather(*tasks)
return results
async def main():
results = await fetch_historical_bars_list(contracts)
for bars in results:
print(bars)
ib.disconnect()
ib.run(main())
上記コードは単に得られたバーデータをそのまま表示しているだけなので、見にくく情報量も膨大になりかねません。適宜必要な処理に変えてください。
その他
ヒストグラム:IB.reqHistogramData()
歩み値(Time&Sales):IB.reqHistoricalTicks()
制限事項
- リクエストは秒間50回に制限されています(Limitations)。ただし、ib_insyncを使用する場合は自動で秒間45回(デフォルト値)に制限されているようなので、特別意識しなくても良いと思われます。
- ストリーミングマーケットデータについては、同時に受信できるのはデフォルトでは100銘柄までとなります(Market Data Lines)。
- ヒストリカルマーケットデータについては、同時にリクエストできるのは最大50となります。
また、30秒足以下のヒストリカルバーデータ(四本値データ)については、以下の制限があります。- 15秒以内に同一リクエストを行う。
- 同じコントラクト、取引所、Tickタイプのリクエストを2秒以内に6回以上行う。
- 10分間以内に 60件を超えるリクエストを行う。
その他、リクエストメソッドによっては制限が設けられている場合がありますので、TWS APIドキュメントで確認してみてください。