3
Help us understand the problem. What are the problem?

posted at

updated at

auカブコム証券のkabuステーションREST APIで自前のリピート注文を作ってみる(リファクタリング編)

はじめに

前記事

  1. auカブコム証券のkabuステーションREST APIをcurlで叩く
  2. auカブコム証券のkabuステーションREST APIをjava(generated by the swagger code generator)で叩く
  3. auカブコム証券のkabuステーションREST APIの残高照会をcurlとjavaで叩く
  4. auカブコム証券のkabuステーションREST APIの残高照会から先物OPのdeltaを計算する
  5. auカブコム証券のkabuステーションREST APIのテスト用モックサーバーを作る
  6. auカブコム証券のkabuステーションREST APIの認証済TOKENをファイル管理する
  7. auカブコム証券のkabuステーションREST APIで自前のトレイル注文を作ってみる(情報収集編)
  8. auカブコム証券のkabuステーションREST APIで自前のトレイル注文を作ってみる(注文発注編)
  9. auカブコム証券のkabuステーションREST APIで自前のトレイル注文を作ってみる(ツール完成編)
  10. auカブコム証券のkabuステーションREST APIで自前のリピート注文を作ってみる(注文約定調査編)
  11. auカブコム証券のkabuステーションREST APIで自前のトレイル注文の改善(トレイル幅の動的拡張)
  12. auカブコム証券のkabuステーションREST APIで自前のリピート注文を作ってみる(設計、検証用実装編)
  13. auカブコム証券のチャートデータの日付を調整する
  14. auカブコム証券のkabuステーションPUSH APIのサンプル

プライベートなソースプロジェクトを乖離してきたので、ソースの見直しを行いながら反映する。

APIラッパー

swaggerがジェネレートしたクライアントAPIをラップしたAPIに委譲させる。

現時点のメリットは、

  • 流量制限にかからない程度にsleepを常に入れる
  • エラー(例外)のスタックトレースを1つのファイルに追記する

将来、検討したい件として、

  • エラーの詳細(どのパラメータが誤りなのか)を保存する →ただしHTTPパケットではBODYにメッセージが入っているはずなので、swaggerのソースの修正かも
  • 条件の和(先物とOPの2つを絞り込み条件にするため、ラッパー内で先物とOPを別々に取得してマージする) →いまは圧倒的に先物しかないので、全部取得しても件数的なメリットはない

クラス一覧

No Name Description Logic
1 ApisoftlimitApi ソフトリミットAPI LockedAuthorizedToken_r4
2 BoardApi 時価情報・板情報API BoardLogic_r4
3 CancelorderApi 注文取消API CloseOrdersLogic_r4,EntryOrdersLogic_r4
4 OrdersApi 注文約定照会API CloseOrdersLogic_r4,EntryOrdersLogic_r4
5 PositionsApi 残高照会API PositionsLogic_r4
6 SendoderFutureApi 注文発注(先物)API CloseOrdersLogic_r4,EntryOrdersLogic_r4
7 SendoderOptionApi 注文発注(オプション)API 未使用
8 TokenApi トークン発行API LockedAuthorizedToken_r4

javaソース

パッケージv9からv9_2に変更し、修正対象のソースをコピーする。
ファイル名をr3からr4に変更する。

追記:通知メールファイルの作成

メール送信自体はWindowsバッチファイルからcurlを呼び出して実行する。
その際に指定するメール本文ファイルをPGM内で作成する。
ファイルの有無でメール送信の判断を行うため、最初にファイルを削除する。

メール本文のサンプル

To: mail_to@example.com
From: mail_from@example.com
Subject: TrailOrder trigger=26860L(70)

2022/05/03 05:28:09.153
---
CLOSE:{日経225mini 22/06  夜間  price=26930S, qty=2, trigger=26860L(70), holdId=E20220506002UH}
---

バッチファイルの抜粋

if exist \tmp\MainTrailOrder_r4.mail (
	curl.exe -u mail_from@gmail.com:PWD "smtps://smtp.gmail.com" --mail-from "mail_from@gmail.com" --mail-rcpt "mail_to@gmail.com" -T "\tmp\MainTrailOrder_r4.mail" --ssl -v -k
)

※問題は2022年5月にgmailから送信できなくなる。

追記:グローバル設定ファイルを管理する

\tmp\Global.cfgを事前に用意し、パラメータを変更できるようにする。

# MailTo,MailFrom,Symbol,SymbolName,ExpireDay,SkipPriceDeltaCeil,SkipPriceDeltaFloor,StopLossPriceRange
MailTo	mail_to@example.com
MailFrom	mail_from@example.com
Symbol	167060019
SymbolName	日経225mini 22/06
ExpireDay	20220513
SkipPriceDeltaCeil	400
SkipPriceDeltaFloor	-100
StopLossPriceRange	1000

GlobalConfigUtilクラスを追加し、Mainクラスから参照する。
設定変更のput()も用意するが、ファイルへの書き込みを用意しないので、PGMから一時的に設定値を変更したいときに使う。

追記:グローバル設定ファイルにメールアドレスを追加

global.cfgへ追加(MailTo,MailFrom)

追記:カンマ文字で分割

タブと同様に行末のカラムが抜け落ちる。
grepしやすいようにsplitTabとsplitCommaを別にする。
StringUtil.splitComma()に置き換え。

追記:2つの文字列に挟まれた文字列を切り出す

自前のJSON文字列の解析用。
StringUtil.parseString(String s, String sl, String sr)に置き換え。

追記:複数の文字列をタブ文字で結合する

2つの文字列の結合と、文字列の配列の結合する。
StringUtil.joinTabに置き換え。

追記:削除したレコードをDELファイルに保存する

物理削除されるため、削除されたレコードを別ファイルに保存する。
ログ扱いとするため、データ本体がタブ区切りであっても、先頭のタイムスタンプはスペース区切りにする。

追記:MainStopLossOrder_r4の発注をStopLossOrderStart設定まで遅延させる。

注文がなければ、すぐにストップロス注文を発注していたが、-1000円の含み損になるまでには時間がかかるため、追加設定StopLossOrderStartの含み損に達するまで注文を遅延させる。

追記:新規発注時に市場コードをチェックしない。

新規発注時に市場コードをチェックしないため、時間外でも発注できる。引数exchangeは不要。

追記:curPrice==0チェック

PositionsLogic_r4のPositionsSuccess.getCurrentPrice()==0チェックと同様に、MainEntryOrder_r4でもBoardSuccess.getCurrentPrice()==0チェックする。

追記:DELファイルのexecutionIdsが空文字列になる。

executionIdsは、APIの状態を確認するため、一方的に保存するのみでファイルから読み込んでいない。
"E2022050902EHR(2-2)"の文字列を解析するのは面倒なので、DELファイル用にexecutionStr変数に文字列を保管する。
toLineString()のexecutionListから文字列を生成する部分をsetExecutionStr()に分離する。

追記:ordersApiとpositionsApiの順番を見直す。

稀に同じ注文が続けて2回発注する障害が発生している。
後からログを追加して、まだ原因は特定できていないが、可能性として、今のソースでは、positionsApiを取得してから、ordersApiを取得しており、positionsApiの結果はMapに入れて、何度も検索している。
現象としては、注文発注が完了(5)した状態で、残高を検索しても該当の建玉が存在しないため、すでに返済済と判断して、2回目の発注を行うことになる。
このため、順番をordersApiの後にpositionsApiを呼び出すように修正する。

特にMainEntryOrder_r4は、refreshOrders()の中でentryOrdersLogic.execute();を実行した後に、execute()でも戻り値を受け取るためentryOrdersLogic.execute();を実行している。refreshOrders()は廃止し、openOrder()のループ前にposLogic.execute();を移動する。

同様にEntryOrdersLogic_r4は、execute()の中で、posLogic.execute();を移動する。

追記:誤発注の原因・・・朝にcurPrice==0のときに残高がクリアされる。

朝ログインし直して、1分ごとに実行していると、7:51に突然残高がクリアされ、

2022/05/13 07:51:05.216 deletePositions(): delete E202205120257Z {name=日経225mini 22/06, price=25920, leavesQty=2}
2022/05/13 07:51:05.216 deletePositions(): delete E2022051202OTL {name=日経225mini 22/06, price=25850, leavesQty=1}
2022/05/13 07:51:05.219 deletePositions(): delete E20220513002BI {name=日経225mini 22/06, price=25590, leavesQty=1}

7:53に残高が追加される。

2022/05/13 07:53:05.235 execute(): create E2022051202OTL {name=日経225mini 22/06, price=25850, leavesQty=1}
2022/05/13 07:53:05.242 execute(): create E202205120257Z {name=日経225mini 22/06, price=25920, leavesQty=2}
2022/05/13 07:53:05.245 execute(): create E20220513002BI {name=日経225mini 22/06, price=25590, leavesQty=1}

positionsAPIの原因かと思ったが、件数は減っていない。

PositionsLogic_r4.execute(): response.size=14

ソースを確認すると、後からcurPrice==0のときに処理を除外していたが、これが原因で決済済と判断されて、注文状況のフラグを5→7に変更してしまう。

			if (type != null && type == 901 && qty != 0 && curPrice != 0) {

未使用IDセットからの削除を、positionsAPIの直後に移動する。

			posKeySet.remove(key);

追記:EntryOrdersLogic_r4.txtレコードの物理削除対応

EntryOrdersLogic_r4.txtのレコードが削除されることなく永遠に残っていたが、ordersAPIから削除された状態のキャンセルSTATE_CANCEL_DELETE(96)と返済済STATE_CLOSE_DELETE(97)を物理削除し、DELファイルに追記する。

ordersAPIから削除されたときの、deleteOrders()のstate状態の遷移は以下のとおり。

state(before) state(after) description
STATE_NOT_ORDER(-1) 変化なし 登録済、未発注のとき、変更なし
STATE_UNKNOWN(0) 変化なし 発注済、注文ステータス不明のとき、変更なし
STATE_WAIT(1) STATE_FINISH_DELETE(95) API:待機(発注待機)のとき、削除済(終了)に変更。おそらく長時間起動していないため、STATE_FINISH(5)を飛ばして削除される。
STATE_SEND_ORDER(2) STATE_FINISH_DELETE(95) API:処理中(発注送信中)のとき、削除済(終了)に変更。一時的なstateのため、まず発生しない。
STATE_ORDER(3) STATE_FINISH_DELETE(95) API:処理済(発注済・訂正済)のとき、削除済(終了)に変更。おそらく長時間起動していないため、STATE_FINISH(5)を飛ばして削除される。
STATE_SEND_CANCEL(4) STATE_FINISH_DELETE(95) API:訂正取消送信中のとき、削除済(終了)に変更。一時的なstateのため、まず発生しない。
STATE_FINISH(5) STATE_FINISH_DELETE(95) API:終了(発注エラー・取消済・全約定・失効・期限切れ)のとき、削除済(終了)に変更。
STATE_CANCEL(6) STATE_CANCEL_DELETE(96) キャンセル済のとき、論理削除する。
STATE_CLOSE(7) STATE_CLOSE_DELETE(97) 返済済のとき、論理削除する。
STATE_FINISH_DELETE(95) 変化なし or STATE_CANCEL_DELETE(96) or STATE_CLOSE_DELETE(97) 論理削除済(終了)のとき、建玉の状態により変更する。
STATE_CANCEL_DELETE(96) 物理削除 論理削除済(キャンセル)のとき、物理削除する。
STATE_CLOSE_DELETE(97) 物理削除 論理削除済(返済)のとき、物理削除する。

追記:CFGファイルの空行をスキップする。

MainEntryOrder_r4.cfgから生成されるMainEntryOrder_r4.txtは、ヘッダ行、L行、S行、最終行の間に空行を挿入しているため、以下の警告が出るため、空行をスキップする。

Warning: SKIP cols.length=1, line=

MainEntryOrder_r4.cfgやGlobal.cfgも同様に空行をスキップする。

追記:デグレード:数量0になっても残高一覧に残る。

curPrice==0のときにposKeySet.remove(key);する影響で、pos.getLeavesQty()==0のときに削除していた動作が、positionsApi.get()から消えるまで、一覧から消えなくなった。

処理対象の条件分岐を、その目的を検証すると、

if (type != null && type == 901 && qty != 0 && curPrice != 0) {
  • SecurityType == null: 先物、OP以外⇒常に除外。posKeySet.remove(key)しない。
  • SerurityType != 901: 日経平均225ミニ先物以外⇒常に除外。posKeySet.remove(key)しない。
  • LeavesQty == 0: 数量0。⇒決済後に除外。posKeySet.remove(key)しない。
  • CurrentPrice == 0: 07:50-08:00ごろに価格0。→一時的に除外。posKeySet.remove(key)する。

条件分岐を2段に分ける。

if (type == null || type != 901 || qty == 0) {
    continue;
}
posKeySet.remove(key);
if (curPrice != 0) {

追記:バグ:逆指値注文がエラーとなる価格の場合は、成行注文する。

5/13夜から5/16昼に変わった時に、MainStopLossOrderの-1000円を超える価格となったため、そのまま逆指値注文をすると、エラーとなる。PLAIN_yyyymmdd_no.logを調べると、以下のメッセージが残っている。

【SendOrderFuture】({"Code":100217,"Message":"ご指定の注文条件ですと、即座に市場に発注されてしまいますので入力し直してください"})

日通しで逆指値の成行ができない理由が、指値でトリガー価格の-100円程度で指値してもそれが約定しない可能性もあるので、成行だとどんな価格になるのか分からないためなのかも。
MainStopLossOrderは最終手段なので、そろそろ日通しに変更しようかと思っていたが、昼/夜それぞれで注文して、その時点で逆指値が出来ない場合は成行にする。

		if (triggerPrice == 0) {
			// 逆指値注文がエラーとなる価格の場合は、成行注文する
			body.setFrontOrderType(120); // 成行
			body.setPrice(0.0); // 成行時0円
		} else {
			body.setFrontOrderType(30); // 逆指値
			body.setPrice(0.0); // 逆指値時0円
			RequestSendOrderDerivFutureReverseLimitOrder rlo = new RequestSendOrderDerivFutureReverseLimitOrder();
			{
				rlo.setTriggerPrice((double) triggerPrice);
				rlo.setUnderOver(StringUtil.underOver(body.getSide()));
				rlo.setAfterHitOrderType(1); // 成行
				rlo.setAfterHitPrice(0.0); // 成行時0円
			}
			body.setReverseLimitOrder(rlo);
		}

追記:MainEntryOrderツールを7:45~8:15の時間帯に非稼働にする。

相変わらず朝一に余計な新規注文が発注されるが、やはり7:51にstateが5→7に変わることにより決済済と判断されている。

2022/05/17 07:51:09.637 execute(): changeState 1652726171046 20220516A02N01494370 5->7
2022/05/17 07:51:09.639 execute(): changeState 1652726290627 20220516A02N01494377 5->7

PositionsLogic_r4.isValidExecutionId()がfalseを返しているので、executionListが無くなっているようだが、curPrice==0となっているので、7:51の時間を処理させないようにする。8:00からstateが1→3に変わっているが、この時間もstate=2や4の可能性があるので、余裕を見て、7:45~8:15の30分は何も処理させないようにする。

  • MainEntryOrder 7:45~8:15以外
  • MainStopLossOrder 8:45~15:05と16:30~29:50
  • MainTrailOrder 8:45~15:05と16:30~29:50

追記:MainEntryOrder_r4.txtにコメントカラムを追加する。

2カラムで

26500,S,1	O,1652746225811

しかないと、他のファイルを参照してデバッグするが大変なので、3カラム目にコメントを追加する。
orderId, state, price(注文) side, executionId:price(約定)xqty

26500,S,1	O,1652746225811	# 20220517A02N01995336, 5,26500Sx1,E2022051700TE6:26500x1

追記:CloseOrdersLogic_r4.txtにコメントカラムを追加する。

上記のMainEntryOrder_r4と同様に、2カラムで

20220517A02N02669402	E202205130134C

しかないと、他のファイルを参照してデバッグするのが大変なので、3カラム目にコメントを追加する。
コメントは、ログやメール本文と同じ文字列を設定する。

20220517A02N02669402	E202205130134C	# CLOSE:{167060019 日経225mini 22/06  夜間  price=26170S, qty=2, trigger=26920L(-750), holdId=E202205130134C}

追記:MainEntryOrderのisValidExecutionId()チェックをPositionsLogicに任せる。

stateの5から6や7の遷移をPositionsLogicに任せ、MainEntryOrder側でstate=5のときのisValidExecutionId()チェックを止める。

追記:entryOrdersLogic.execute()を2回呼ぶ。

MainEntryOrder_r4.openOrder()の中で新規注文を追加した後に、execute()にあるList entryListのリストは追加される前のリストのため、STATE_NOT_ORDER(-1)のときに発注されるように見えて、次のターンまで遅延するため、entryOrdersLogic.execute()を2回呼ぶ。

追記:fix curPrice==0のときにEntryOrdersリストがすべて消える。

朝にならないと分からないcurPrice==0のときに、entryOrdersLogicの初期化されていない状態で、entryOrdersLogic.writeOrders();を呼ぶと、すべて消える。

追記:インスタンス生成するユーティリティをロジックにリネームする

パッケージ名、クラス名で区別するため、ユーティリティ(util)の中で、インスタンス生成するものはロジック(logic)にリネームし、パッケージ移動する。

  • SendMailUtil to SendMailLogic

追記:LockedAuthorizedToken_r4をFileLockUtilからFileLockLogicに置き換える。

LockedAuthorizedToken_r4をFileLockUtilからFileLockLogicに置き換える。

追記:ソース履歴の不要なクラスをlogicやutilへ移動する。

r4, r5とソース履歴を取る必要のないクラスを共通パッケージ(logic, util)へ移動する。

  • move v9_2.BoardLogic_r4 to logic.BoardLogic
  • move v9_2.SendOrderConfig_r4 to util.SendOrderConfigUtil
  • move v9_2.LockedAuthorizedToken_r4 to util.LockedAuthorizedTokenUtil

追記:fix EntryOrdersから削除されるとstateが遷移できなくなる。

O,XXXのままリンク切れとなっていることがある。
原因は、XXXが決済済で、新たにYYYを発注する予定が、値幅が大きいため注文しない状態のまま放置していたところ、ordersAPIから削除されるので、リンク切れとなる。

O,XXXがリンク切れの場合、Rに戻す。
P,XXXがリンク切れの場合、Pに戻す。
C,XXXがリンク切れの場合、Pに戻す。

追記:pom.xmlにjavax.annotation-api 1.3.2を追加する。

新ノートPCに新eclipseを入れたら、JDK 8を拒絶され、JDK 11に上げたので、pom.xmlにjavax.annotation-api 1.3.2を追加する。

追記:現物株ではInteger cashMargin==nullとなる。

現物株を扱わず先物OP前提だったので、なかなか気がつかなかった問題として、OrdersAPIが返すInteger cashMarginについて、取引区分(2:新規、3:返済)となっていて、現物株の場合はnullとなる。
ただ、SendOrderAPIで渡すcashMarginは信用区分(1:現物、2:新規、3:返済)となっているので、1を返してくれてもよいのですが。

Integer obj = null;

int val = obj;
するとオートボクシングでNullPointerExeceptionとなります。

ただ、
if (obj == 2) {
は、まずobj != nullチェックがかかってから、(int)obj == 2の判定となるようです。

==演算子は参照の同一性比較をInteger式で実行し、値の等価性比較をint式で実行します。
Autoboxing

equalsの実装では、nullのときinstanceofがfalseなので、キャストされることはありません。

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

githubソース

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
3
Help us understand the problem. What are the problem?