証券Fintechに入ったエンジニアが証券ドメインの業界知識を身に着けたいと思って読んだ本

これは何

  • 証券Fintechに入るぞ・入ったぞ!という人がドメインについて学べるような本・ページを集めてみました。
  • 会社特有の業務知識ではなく業界で広く使える知識を学べる物を集めようとしていますが、筆者の守備範囲依存による分野の偏りは否めません。
  • こういう分野があってこういうことが議論されてるんだなーといった形で興味を持つきっかけとしての本を挙げています。(自分で金融商品を考えるぞ!という人向けではなく、ETF扱うサービスの開発するならETFって何かくらいはうっすら知っておきたいよねーくらいのモチベーションの人向き)

最初に何読めばいい?

せっかくだからなにか勉強したいなーという人は資格系の欄にある証券外務員についての本を買っておくと広めに勉強できます。

ただし、個別の金融商品ETF, 投資信託,…)の解説が詳しく載っているわけではないので、そういったことを知りたい場合はETF投資信託の欄からチョイスしてみてもよいかもしれません。

資格系

現物株

投資一任, ロボアド

  • ラップ口座入門
    • ラップ口座の拡大・現在の状況・今後の方向性がファクトベースで記載されている。
    • 投資の基本的なところから説明してくれているので事前知識があまりなくても読める。

投資信託

ETF

資産運用の考え方について

決済系

金商法

  • 金商法入門
    • 解説対象を現物株に限ることで難しさを軽減している。
    • 法の背景にあるやりたいこともきちんと解説してくれる。

リスク管理

aws s3でオブジェクトkeyの先頭にスラッシュ(/)をつけるとどうなるか?

これはなに

  • s3はbucketとkeyの組み合わせでオブジェクトを保存している
  • このbucketとkeyを s3://$bucket/$key のように組み合わせると S3URIとなる
  • keyの先頭のslashがついているとき、aws cliを含む一部のツールでは 先頭のslashを削除する場合がある。
  • この仕様についての記載はaws cli側のドキュメントでは見つからなかったが、本記事では go-sdk での命名に習ってURI cleaningと呼ぶことにする。
  • この場合、ファイルは存在するように見えるが取得しようとすると NoSuchKey が返却されるといった事象が起きる可能性があるので、それについて解説する

HeadObjectの仕様

まずファイルの存在確認ではHeadObjectを利用することが多いので、そこの仕様を確認する。

HeadObject - Amazon Simple Storage Service を読むと

Permissions

You need the relevant read object (or version) permission for this operation. For more information, see Specifying Permissions in a Policy. If the object you request does not exist, the error Amazon S3 returns depends on whether you also have the s3:ListBucket permission.

If you have the s3:ListBucket permission on the bucket, Amazon S3 returns an HTTP status code 404 ("no such key") error.

If you don’t have the s3:ListBucket permission, Amazon S3 returns an HTTP status code 403 ("access denied") error. 

とある。つまり

  • ListBucket権限がありファイルがないなら404
  • ListBucket権限がないなら403 (access denied)

が返ってくる。

aws java-sdk v2 などでは 404の場合 NoSuchKeyExceptionthrowされ、それ以外の場合は SdkClientException などがthrowされる。

そのため NoSuchKeyException がthrowされている場合は、権限不足ではなくファイルが実際に存在していないことを疑ってもよいだろう。

HeadObjectをcliで叩く

HeadObjectをaws-cliで叩くためには通常の aws s3 <command> ではなく aws s3api <command> という低レベルAPIを直接たたくコマンドを利用する必要がある。(詳細は AWS CLI での API レベル (s3api) コマンドの使用 - AWS Command Line Interface)

head-objectコマンドを利用する場合はkeyとbucketを以下のように分けて指定することが可能なので、先頭にスラッシュを含まないファイルと含むファイルの存在を以下のようにチェックすることができる。

# keyの先頭にスラッシュなし
aws s3api --profile ... head-object --bucket some-bucket --key some/key/foo.json

# keyの先頭にスラッシュあり
aws s3api --profile ... head-object --bucket some-bucket --key /some/key/foo.json

もし後者の先頭スラッシュつきでファイルが存在するレスポンスが返ってきたらURI cleaningの影響でファイルが見つからない状態になっている可能性が高い。

余談

  • 基本的にHeadObjectはGetのbodyを取得しない版と同等なので性能を気にしなくていい場合はaws s3 cp $file . などでファイルを取得してもよい。
  • 今回はURI cleaningを避けるためにs3URI形式ではなくbucket, keyを分割して渡すようにする目的で低レベルAPIを利用している。

各ツールの仕様

java-sdk

java-sdkでは、bucket, keyを分けて put, get する仕様になっている。

このとき、key側の先頭slashが消されると言った挙動は(少なくともHeadObjectでは)確認できなかったのでURI cleaningの罠を踏むことはなさそう。

val request = HeadObjectRequest.builder().bucket("some-bucket").key("some-key").build()

go-sdk

go-sdkでもbucket, keyを分けてput, getする仕様になっている。

しかし Automatic URI cleaning という仕様があり、key先頭のslashは削除される。

この挙動は DisableRestProtocolURICleaning をtrueに設定すると回避できる。

svc := s3.New(sess, &aws.Config{
      DisableRestProtocolURICleaning: aws.Bool(true),
})
out, err := svc.GetObject(&s3.GetObjectInput {
      Bucket: aws.String("bucketname"),
      Key: aws.String("//foo//bar//moo"),
})

aws-cli

aws-cliでは s3://$bucket/$key という形式でget, putすることになる。

このとき key先頭のslashが削除されるために先頭にslashが入っているオブジェクトは取得ができないことがある。

なお、先頭slashは1個だけ削除される仕様ではなく、複数個削除されるようになっているため、以下のように $bucket//$key のような文字列を指定しても、ファイルは存在しない扱いになる。

$ aws s3 --profile ... ls s3://some-bucket//some/key/foo.json

web-console

試してないので誰か教えて ( ◜◡‾)

結論

keyの先頭にスラッシュをつけるとファイルを手動操作したい場合に余計な混乱を呼ぶのでやめたほうが無難そう。

※ s3から静的ファイルを配信する際のURLも難しくなってしまうので、そういう意味でも避けたほうが良さそう

sbt-native-packagerでパッケージングしたアプリケーションのJVMオプションに-Jをつけるべきかどうか

普通に自分が分かってなかったのでメモシリーズ

これはなに

  • -Xmx オプションを指定する際に -J をつけている。 -Dfoo.barなどのプロパティ指定にはつけたりつけなかったりしている
  • いつ -J をつけて、いつつけなくてよいのか、 -J の意味などをまとめる。

-J の意味

-J の意味1 javaコマンドのオプションである -J

java -J” あたりでググるとコレがでてくる。 例: Javaアプリケーション メモ(Hishidama's Java-Application Memo)

これはjavacが使用するjavaコマンド(javacと同バージョンのjava)へのオプションを指定するものなので、Xmxの指定などの実行時のオプション設定には関係しない。

-J の意味2 jarコマンドのオプションである -J

こちらもググってみると、

たとえば、 -J-Xms48m と指定すると、スタートアップ・メモリーは48Mバイトに設定されます。

jar のように記載されている。

しかし「jarコマンド」はjarを作ったりjarの中身を一覧表示するためのコマンドであり、jarの実行(java -jar …)とは異なる物なのがポイントで、こちらもアプリケーション実行時に影響するオプションではない。

つまり jar -J-Xms48m はjarを作る作業中にjvmが使うメモリを変更するオプションであるという意味になる。

-J の意味3 scalaコマンド的な-J

scala -J-Xmx 2GB -Dfoo=bar baz.scala みたいにjavaコマンドではなくscalaコマンドでアプリケーションを実行するときのオプション

scalaコマンドは結局モロモロの処理をした後javaコマンドを実行するものになっている。

ここでのポイントは-Jオプションをscalaコマンドに渡すと以下の2つを行ってくれるということ

  • 渡された -J-Xmx1500m などの文字列をそのままjavaコマンドに渡す
  • -J を消した-Xmx1500mをscala引数として渡す

つまり -J の意味1 で述べたjavacうんぬんとは関係なく、scalaコマンドに -J を渡すとjava側に渡してくれる機能がある( -Jという名前が被っているので混乱するが、全然違う機能) scala/tool-unix.tmpl at c804289144c006e91a206ff12e94e5a39ac73a9f · scala/scala · GitHub

-J の意味4 sbt-native-packager的な -J

こちらはscalaコマンドに似ている。

sbt-native-packagerが生成するスクリプトはモロモロの処理をしてjavaコマンドを実行する。

このとき -J の意味2で述べたscalaコマンドの挙動を模した実装が行われており、scalaコマンドと同じように -J をstripして、 -Xmx 1500m にしてからjavaコマンドにわたすという形式になっている。

(scalaコマンドとは違いscala_argsには値が渡らない気がしているが未調査。気になる人は実装読んだりしてください)

sbt-native-packager/bash-template at master · sbt/sbt-native-packager · GitHub

-J をつけないとどうなるかというと sbt-native-packager/bash-template at 1ee84811c3ce2048e2ea857aece3fbe563b8089e · sbt/sbt-native-packager · GitHub によりresidual_argsに入り sbt-native-packager/bash-template at 1ee84811c3ce2048e2ea857aece3fbe563b8089e · sbt/sbt-native-packager · GitHub でmainClassやappCommandの後に引数として渡されるようです。

まとめ

sbt-native-packagerを使う場合、jvmオプションは -J-Xmx 1500m のようにする必要がある

これはscalaコマンドやsbt-native-packagerが独自実装しているshellスクリプトの機能でしかなく、jvmには-Jが削除された-Xmx 1500mだけが渡るようになっている。(-J-Xmxってなんか気持ち悪いなと思っていたけど-Jという文字列を機械的に削除すると考えれば割と理解しやすい)

この際使われる-J prefixはjavaコマンドについてググった時にでてくるjavacコマンドのオプションとは全く別物

おまけ

sbt-native-packagerを使う場合 -Dfoo=bar はそのままjvmに渡される (sbt-native-packager/bash-template at 1ee84811c3ce2048e2ea857aece3fbe563b8089e · sbt/sbt-native-packager · GitHub) ので、

例えばエンコーディング指定であれば -Dfile.encoding=UTF-8 とすればよい。

が、 -J-Dfile.encoding=UTF-8 としても -J機械的に削除されてjvmに渡るので結局 -Dfile.encoding=UTF-8となり、挙動はかわらない(はず)。

Error downloading org.scalameta:semanticdb-scalac_2.13.7:4.4.28 や scalafix-test-kit_2.12.13:0.9.29 と言われたら

この記事について

  • scalafixに依存しているプロジェクトでscalaのバージョンを上げるとタイトルのエラーが出る場合があるので原因などを整理したもの
  • ちょっと前のメモ書きをブログ化してるので各種バージョンは古いです

各エラーについて

エラー1: Error downloading org.scalameta:semanticdb-scalac_2.13.7:4.4.28 と言われたら

原因

scalafixが依存しているsemantic DBのライブラリが存在していないことが原因

semanticdbのバージョンは Installation · Scalafix に従っていれば以下のようになっているはず。

semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision

semanticdbVersion は実際には下記の様に指定されている。

https://github.com/scalacenter/sbt-scalafix/blob/b7b890d00598a574b9d4ea88e4f42168cc83765c/src/main/scala/scalafix/sbt/ScalafixPlugin.scala#L91-L94

    val scalafixSemanticdb: ModuleID =
      scalafixSemanticdb(BuildInfo.scalametaVersion)

    def scalafixSemanticdb(scalametaVersion: String): ModuleID =
      "org.scalameta" % "semanticdb-scalac" % scalametaVersion cross CrossVersion.full

この例では実際には org.scalameta:semanticdb-scalac_2.13.7:4.4.28 と解決される(細かいバージョンはscalaとsbt-scalafixのバージョンで変化する)

今回の細かい値は以下のようになっていた。

  • scalaVersion=2.13.7
  • sbt-scalafix version=0.9.31

このライブラリがmaven上にpublishされていないことが依存が解決できないのが表題のエラーの原因。

publishされているかの確認は https://repo1.maven.org/maven2/org/scalameta/semanticdb-scalac_2.13.7/ に存在しているかで行える。

解決方法

単純にsemanticdbが存在しないことが原因なので、すでにpublish済みのsemanticdbバージョンを使えば良い。

sbt-scalafixでは scalafixSemanticdb(scalametaVersion: String) で指定できるので、以下のように好きなように指定すれば良い。

import scalafix.sbt.ScalafixPlugin.autoImport.scalafixSemanticdb

...
semanticdbVersion := scalafixSemanticdb("4.4.30").revision,

指定するバージョンは Central Repository: org/scalameta から semanticdb-scalac_${使いたいscalaバージョン} 配下にある最新のversionのものにすればよい。

例: Central Repository: org/scalameta/semanticdb-scalac_2.13.7/4.4.30 なら 4.4.30 を指定する。

余談

余談1: ワンポイントsbt

scalafixのコードのなかで CrossVersion.full という指定が出てきていたので意味を整理しておく。

ライブラリのバージョン指定時に CrossVersion.full が指定されていると "org.scalameta" % "semanticdb-scalac" % バージョン番号"org.scalameta" % "semanticdb-scalac_<scalaバージョン>:バージョン番号 として解決され、最終的に org.scalameta:semanticdb-scalac_2.13.7:4.4.28 という指定になる。

普段依存ライブラリを指定する場合は "org.scalameta" % "semanticdb-scalac" %% バージョン番号のように %% を使った指定を行うことが多いが、この場合は "semanticdb-scalac_<scalaバイナリバージョン>:バージョン番号 のように解決され、最終的に org.scalameta:semanticdb-scalac_2.13:4.4.28 のようにバイナリ互換性のあるバージョンまで(2.13.7ではなく2.13まで)を指定するようになっている。これは CrossVersion.binary を指定するのと等しい挙動になる。

CrossVersionには他にCrossVersion.disableも存在するが、これは単にscalaバージョン番号を埋め込んでいないもの(javaのライブラリなど)の指定に用いる。

※ 通常はjava製ライブラリは%、scala製ライブラリは%%を使うと覚えておくと良い。

参考

余談2: scalametaのバージョンはどう指定されているか?

sbt-scalafix version=0.9.31 の場合、scalametaのバージョンは4.4.28と解決される。

これは

をみると scalafix-interfaces.properties というリソースの scalametaVersion に記されていることが分かる。

このファイルの場所はsbt-scalafixに依存している適当なプロジェクトで以下のようにすれば特定できる。

$ sbt
> reload plugins
...
> console
scala> getClass.getClassLoader.getResource("scalafix-interfaces.properties")
res0: java.net.URL = jar:file:/略/https/repo1.maven.org/maven2/ch/epfl/scala/scalafix-interfaces/0.9.31/scalafix-interfaces-0.9.31.jar!/scalafix-interfaces.properties

が、 GitHub - scalacenter/scalafix: Refactoring and linting tool for Scalaリポジトリを見てみるとそういったファイルは存在しない。

こういった場合は自動生成されている可能性があるので build.sbt を探すと以下のように自動生成を行うタスクの記述がある

scalafix/build.sbt at v0.9.31 · scalacenter/scalafix · GitHub

これを見ると scalametaV という変数を参照していることが分かるので

scalafix/Dependencies.scala at v0.9.31 · scalacenter/scalafix · GitHub

にある

  val scalametaV = "4.4.28"

が該当する。

エラー2: not found scalafix-test-kit_2.12.13/0.9.29と言われたら

現象

  • scalafix(scalafix-testkit)はscalaのメジャーバージョン(scala 2.12 or 2.13)だけでなくマイナーバージョン (2.12.13 or 2.12.14)のレベルまで依存している。
  • scalafixとscalaのバージョンの組み合わせが悪いのでnot foundになっている

解決方法

以下のような要領で使えるバージョンを探しましょう

BiTemporal Data Model導入時の注意点

これはなに

  • BiTemporal Data Modelはこういうことができるよ!という内容ではなく、導入時の注意点やちょっとしたつまづきポイントなどをまとめたもの

背景

BiTemporal Data Modelについて、すでに何社かでは導入事例もあるようで*1、たまに BiTemporal Data Modelに入門中 - だいたいよくわからないブログ を参照していただくこともあるようです。

そんな中でBiTemporal Data Modelについてこういうことができてすごいよ!という話だけでなく、導入するとこういうところが辛いとかそういうこともまとめておきたいなと思った次第です。(割とぱっと思いつくところをメモっている感じなので思い出したら追記するかも)

  • DBはMySQLを想定しています。
  • activated_at, deactivated_atをビジネス時間, in_z, out_zを処理時間のカラム名として利用しています。
  • 説明のためactivated_at, deactivated_atが日付になったり日時になったりしています。(実際には日時で管理)
  • 小ネタ集なので技術的にいくらでも回避できるだろうということも一応書いています。(ちょっとだけ考えることが色々ある。というのも注意点にはなると思うので)
  • ネガキャンみたいな記事ですが、これさえ使えば完璧に幸せになれる!ってわけじゃないよ。というのが趣旨なのでBiTemporalを便利に使えるケースもいっぱいあると思います。自分の状況に合うと思ったら使い、合わないと思ったら別の手段を検討するのが良いと思います。

SQL where条件つけ忘れリスク

論理削除についての議論でもあると思うのですが、SELECT時の条件としてout_z=MAXを付け忘れている場合は当然削除されたはずのデータが処理対象になってしまいおかしなことが起こります。

さらにいうとout_z < MAXなデータって通常の本番運用ではあまりないといった使い方も結構ある(state遷移はactiavted_at,deactivated_atでやるのでout_zは障害対応でレコードを消す場合にしか使いません。など)ので、障害対応したら更に障害になった。。。ということも考えられます。

  • out_z < MAXなデータをfixtureに含めたテストを行うことを検討してみると良いかもしれません
  • レビュー時のチェックリスト・オペレーションでSQLを実行する前のチェックリストなどを作成するといったことをしてもいいかもしれません。
  • 実装時のエンバグについてはreladomoなどの対応しているORMを利用することで回避することが可能だと思います
  • リスク・コストとリターンを照らし合わせて履歴・訂正テーブルなどを作り込むことも検討しましょう。

学習コスト

初見時はやっぱり難しいです。勉強用リンク集を整備したり、練習問題つきのサンプルリポジトリがあると新規加入メンバーがキャッチアップしやすいかもしれません。

大きなシステムで大々的に使う場合は良いのですが、小さくあまりいじらないシステムで開発者が転職...といったことになると後任の運用担当者が「なにこれ・・・」となる可能性も否めません。 関わる人が幸せになるように、運用のハードルが低いシステムにするためにbitemporalを採用しないというのも選択肢としては考慮しましょう。

ユニークキーが効かない(効かせにくい)

PostgreSQLなどだと範囲型がある ので試したことはないんですが困らないんじゃないかなと思います。

例えば以下のような商品ごとの設定値を管理するレコードがあったとします。

item_id 設定値 activated_at deactivated_at in_z out_z
1 true 1990/1/1 MAX ... MAX

そして、この設定値を2021/1/1からfalseにしたいとします。 正しい手順としては以下のようになります。

item_id 設定値 activated_at deactivated_at in_z out_z
1 true 1990/1/1 2021/1/1 ... MAX
1 false 2021/1/1 MAX ... MAX

ここで誤って以下のように設定値=falseのレコードを1995/1/1から有効として登録してしまったとします。

item_id 設定値 activated_at deactivated_at in_z out_z
1 true 1990/1/1 2021/1/1 ... MAX
1 false 1995/1/1 MAX ... MAX

上記のテーブルをas_of_time = 2010/1/1 で引くとマスターデータが2件取得される現象が起こります。(あたりまえ)

この手のテーブルだと有効な設定値が一件であることをunique keyなどで保証することが多いと思いますが、activated_at, deactivated_atで管理している場合はせいぜい (item_id, deactivated_at) のunique keyを付けてdeactivated_at=MAXなレコードが2件ないことをチェックする程度の制約になりそうです。in_z, out_zに関してはout_z=MAXのレコードが唯一存在していればいいという割り切りで十分ですが、activated_at, deactivated_atの場合はそうも行かないので注意が必要です。任意のas_of_timeについてactivated_at, deactivated_atの区間に重複がないことをチェックできないタイプのRDBMSでは注意が必要です。*2

性能面のデメリット

そりゃそうだろって話なんですがstatusテーブルをbitemporal data modelで管理するといったことを考えると、status更新時には以下の二行の操作が必要です。

  • 旧レコードで UPDATE ~~ SET deactivated = ...;
  • 新レコードを INSERT ~~~;

単純にsnapshot modelで検索するのに比べて更新コストが高くなりますし、テーブルの件数も増えるのでselectの負荷もかかるかもしれません。パフォーマンス命!の場合はログに残すなどの方法も検討できるかもしれません。

activated_at == deactivate_atの場合の取り扱い

これは細かいことではあるんですが

  • 旧レコードで activated_at ~ deactiavted_at = 2000/1/1 00:00:00 ~ 2009/1/1 10:18:15
  • 新レコードで activated_at ~ deactiavted_at = 2009/1/1 10:18:15 ~ 2010/1/1 00:00:00

のようなレコードを想定した場合、as_of_time=2009/1/1 10:18:15 でデータを取得したときに旧レコードが取れるのか、新レコードが取れるのか、二件取れるのか?は当然ながら実行されるSQLに依存します。

  • activated_at < ${as_of_time} AND ${as_of_time} <= deactivated_atなら旧レコードが取れる
  • activated_at <= ${as_of_time} AND ${as_of_time} < deactivated_atなら新レコードが取れる
  • activated_at <= ${as_of_time} AND ${as_of_time} <= deactivated_at だと二件取れる

どういうSQLにするにしろ、ある程度方針を決めておかないと気づいたらアプリケーションごとにバラバラになっていたというのも新しいメンバーにとっては認知コストの上昇につながるので注意しておくとよいかもしれません。

また2件取れて嬉しいことは少ないと思うんですが $as_of_time BETWEEN activated_at AND deactivated_at のようなクエリにすると2件とれる状態になるのでご注意ください*3

同一時刻で二回アップデート

ちょっと特殊なユースケースでしか発生しないと思うのですが、ある種のバッチなどでstatusを 1 => 2 => 3 => 4と一気に遷移させつつ、履歴は全部残したいみたいなことが限定的な状況下ではあったりするかもしれません。*4

as_of_time=2021/1/1 15:15:15時点で上記のようなstate遷移を行うバッチをシンプルに実装して実行すると以下のようになるかもしれません。(もちろん実装による)

item_id status activated_at deactivated_at in_z out_z
1 1 1990/1/1 00:00:00 2021/1/1 15:15:15 ... MAX
1 2 2021/1/1 15:15:15 2021/1/1 15:15:15 ... MAX
1 3 2021/1/1 15:15:15 2021/1/1 15:15:15 ... MAX
1 4 2021/1/1 15:15:15 MAX ... MAX

ここで困るのは二点

  • ユニークキーとして(item_id, deactivated_at)を付けている場合にduplicate entryになってしまう。
  • as_of_time=2021/1/1 15:15:15時点で有効なレコードが(SQLの条件次第では)複数件存在してしまう。(このバッチではなく、他の処理のSQLに依存してしまうため調査が必要になるカモ)

これですごい困ったことはないんであれなんですが、as_of_timeをちょっとずつずらしながら変更するみたいなハックを試みても良いかもしれません(その処理が重要ならもう少し考えても良いかもしれませんが)

またactivated_at, deactivated_atを秒精度で保存していると高速に呼ばれるAPIでも踏むことがあるかもしれません。ミリ秒精度(もしくは更に細かく)での保存・管理を検討してみてもよいでしょう。

MAXの値がUTCに変換されてしまう

MAXの値としては9999-12-31 23:59:59などを利用するとことが多いと思うのですが、アプリケーションの実装次第では9999-12-31 14:59:59 などUTCに変換された値がDBに入ることがあります。 これはまあテストで気づくでしょって感じなんですが、実装次第ではこういう事が起きるので確認したほうが良いこととしては挙げておきます。

*1:1986年の論文とかも普通にあるようですし昔からやってたよというところももちろんあるでしょう

*2:イケてるTemporal DB 情報お待ちしております

*3:完全な余談ですが債権の金利計算では両端の考慮について、それぞれを前落片端・後落片端・両端と呼んで区別するそうです。債券計算サイト

*4:テスト環境でテストデータを作成するためにそれっぽい状態を作りこむバッチとか