前回の記事の続きです。
今回は、IB APIの仕組みを見て、呼び出しの共通化を考えます。
なお、サンプルとして資産情報の取得を行います。
相変わらず長くなるので、オプション価格の取得やオーダーは次回以降に回したいと思います。
対象者
- 株式/先物/オプションなどを自動売買したい
- 売買アルゴリズムのカスタマイズを自由にしたい
- Java/C++/C#など、使い慣れた言語で書きたい
- IB証券(LLC)の口座を持っている
前回と同様です。
APIの仕組み
IB APIはほとんどのリクエストが非同期です。
つまり、こちらがリクエストを投げてもすぐに結果は返ってきません。
コールバックをうまく処理して、目的のデータを取得する必要があります。
サンプル
例えば、アカウントの資産情報を取得する流れを見てみます。
まず、IBクライアントに「資産情報ください」とリクエストを投げます。
マニュアル
val requestTags = List(
"NetLiquidation",
"InitMarginReq",
"SMA"
)
socket.reqAccountSummary( client.getNextValidId(), "All", requestTags.mkString(",") )
getNextValidIdは、まだリクエストに使っていない整数値を返すメソッドです。お好きな方法で自作してください。
tagには欲しい情報を指定します。
このリクエストを実行すると、少し時間が経過した後、対応するメソッドが呼ばれます。
どれが呼び出されるか分からない場合は、マニュアルを読むか、実際に読んでみて手元で確認してみてください。
今回対応しているメソッドは
accountSummaryです。
これがtagの個数だけ呼びだされます。
また、全て列挙が終わったら、accountSummaryEndが呼び出されます。
なお、マニュアルに明示的に書かれていないので、実際に呼んでみて試すのが楽です。
コールバック関数は、前回同様以下のように実装しました。
data.accountInfoなるクラスにデータを詰め込んでいます。
設計に応じて置き換えて読んでください。
override def accountSummary(reqId: Int, account: String, tag: String, value: String, currency: String): Unit = {
tag match {
case "InitMarginReq" =>
data.accountInfo.initMarginReq = Some(value.toDouble)
case "NetLiquidation" =>
data.accountInfo.netLiquidation = Some(value.toDouble)
case "SMA" =>
data.accountInfo.SMA = Some(value.toDouble)
}
}
override def accountSummaryEnd(reqId: Int): Unit =
data.accountInfo.complete = true
API呼び出しの共通化
上記で見てきたように、req~~メソッドと~~Endコールバックが開始と終了で、その間に実際にデータが送信されるコールバックがあるという仕組みになっていました(例外もありますが、おおよそこの形です)。
そのため、このようなインターフェースを用意して、リクエストごとにサブクラスを用意し実装すれば上手くいきます。
abstract class Request {
protected def preRequest()
protected def request()
protected def postRequest()
final def run() = {
preRequest()
request()
// 待つ
postRequest()
}
}
もともと非同期なので、ScalaなどではFutureやActorなどで簡単に並列化ができます。
本記事ではわかりやすさのため、ブロッキングして結果が返ってくるまで待つパターンで実装してみます。
単純な自動売買であれば、のんびり同期通信をしても大抵問題ないかと思います。
とはいえ、オプション価格の取得はとりわけ遅いので、複数銘柄を扱う際は並列化した方が良いです。
実装サンプル
以下は実装のサンプルです。
お使いの言語や設計に合わせて変更してください。
まずは、所定のメソッドがtrueを返すまで待つメソッドを適用に用意します。
数秒に一度読込中メッセージを流したかったため、以下のような形式んしています。
object Util {
val GIVE_UP_TIME = 20 * 1000 // millisecond
val INTERVAL_TIME = 100 // millisecond
def wait(condition: () => Boolean)() = {
(1 to GIVE_UP_TIME / INTERVAL_TIME).toStream
.takeWhile(_ => !condition()).foreach(i => {
if (i % 30 == 0)
println("Waiting...")
Thread.sleep(INTERVAL_TIME)
if (i * INTERVAL_TIME >= GIVE_UP_TIME)
throw new TimeoutException("give up...")
})
}
}
次に、リクエスト全般のインターフェースを用意します。
簡単に、complete変数がtrueになったら待機を終了、という挙動をデフォルトにしておきます。
varなのはご容赦ください……。
abstract class Request {
var complete = false
protected def isComplete = complete
protected def preRequest()
protected def request()
protected def postRequest()
final def run() = {
preRequest()
request()
Util.wait(isComplete _)()
postRequest()
}
}
次に、リクエストごとの実装を行います。
今回はサンプルでも扱ったAccount情報を取得します。
printlnに放り込んだ時にデータを出力して欲しいので、toStringもオーバーライドしておきます。
varだらけなのは(ry
class Account(client: IBClient) extends Request {
val requestTags = List(
"NetLiquidation",
"InitMarginReq",
"SMA"
)
var netLiquidation: Option[Double] = None
var initMarginReq: Option[Double] = None
var SMA: Option[Double] = None
override protected def preRequest(): Unit = {}
override protected def postRequest(): Unit = {}
override protected def request(): Unit =
client.socket.reqAccountSummary(client.getNextValidId(), "All", requestTags.mkString(","))
override def toString =
s"netLiquidationValue: ${netLiquidation}, initMarginReq: ${initMarginReq}, SMA: ${SMA}"
}
さて、あとはコールバックが実装されていれば、以下のように呼び出せば情報が取得できるまで待機し結果を出力してくれます。
val accountInfo = new Account(client)
accountInfo.run()
println(accountInfo)
実行結果は、以下のように出力されます。
ペーパートレーディング口座なので、仮想通貨がたくさん入ってますね。
netLiquidationValue: Some(1.1247504391E8), initMarginReq: Some(1575432.69), SMA: Some(2.423691458E8)
まとめ
APIの仕組み、リクエストの共通化ができました。
様々な情報を取得できるようになったので、ポートフォリオ情報などを取得してみると良いかと思います。
次回はオプションの価格情報を取得したいと思います。