Haskellで株のシステムトレードプログラムを作る。(まだまだ途中)

前回までのあらすじ

素人のくせに安易に手を出した株取引で、趣味に使うつもりの財産を失ってしまう。
痛いが敗北を認めて投資を縮小し、エアコンをつけたり、スイッチやコンセントや照明をつけたりする工事を頑張りながらこつこつ資本を蓄積して、次回は機械力を活用してライバルに差をつけるのだ。

Haskellに入門ついでにPythonに替えて株のシステムトレードプログラムを作る。

MariaDB(MySQL)から取り出した株価からテクニカル指標を計算してチャートをJupyter NotebookとExcel両方で描いてみた。

こんな感じで休日にぼちぼち進行中。

commit-graph-image
サンデープログラマなのに何故か木曜日も多かったりする。

バージョン0.3

一段落ついたので、バージョン0.3にしました。
変更点

    • コンパイルフラグ-Wallをつけて、出てくるwarningに対処しました。
    • stylish-haskellとhlintを使うようにした。
    • stack 1.6.1 にしました。
    • LTS Haskell 10.0 (ghc-8.2.2)にしました。
    • Sqliteから全面的にMariaDB(MySQL)を使うようにしました。
    • テストケースをつけてみました。
    • フォルダ構成を整理しました。
    • 正確なスケジューラを用意しました。
    • 失敗後の再復帰をかえました。
    • 平日と土日曜日とを分けました。

興味があるなら、GitHubにあるので手元で検証してもらえればいいと思う。

用意する物

Linux環境
$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.3 LTS"
MariaDB(MySQL)
$ mysql --version
mysql  Ver 15.1 Distrib 10.0.31-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
tmuxとかscreenとかがあれば便利かな
$ tmux -V
tmux 2.1
Git
$ git --version
git version 2.7.4
松井証券のアカウント
用意すること。
Slackのアカウント
用意すること。

手順

stack経由でghcを入れるので、まずstackの用意を。
無いなら公式の”How to install”どおりstackのインストールをする。

$ curl -sSL https://get.haskellstack.org/ | sh

適当なディレクトリでgit cloneすると、こうなる。

$ git clone https://github.com/ak1211/tractor
$ cd tractor
$ tree
.
├── app
│   └── Main.hs
├── doc
│   ├── Aggregate.html
│   ├── Conf.html
│   ├── DataBase.html
│   ├── doc-index-A.html
│   ├── doc-index-All.html
│   ├── doc-index-B.html
│   ├── doc-index-C.html
│   ├── doc-index-D.html
│   ├── doc-index-E.html
│   ├── doc-index-F.html
│   ├── doc-index-G.html
│   ├── doc-index-H.html
│   ├── doc-index.html
│   ├── doc-index-I.html
│   ├── doc-index-J.html
│   ├── doc-index-K.html
│   ├── doc-index-L.html
│   ├── doc-index-M.html
│   ├── doc-index-N.html
│   ├── doc-index-O.html
│   ├── doc-index-P.html
│   ├── doc-index-R.html
│   ├── doc-index-S.html
│   ├── doc-index-T.html
│   ├── doc-index-U.html
│   ├── doc-index-W.html
│   ├── haddock-util.js
│   ├── hslogo-16.png
│   ├── index.html
│   ├── Lib.html
│   ├── Main.html
│   ├── minus.gif
│   ├── Model.html
│   ├── ocean.css
│   ├── plus.gif
│   ├── Scheduling.html
│   ├── Scraper.html
│   ├── SinkSlack.html
│   ├── StockQuotesCrawler.html
│   ├── synopsis.png
│   ├── TechnicalIndicators.html
│   ├── TickerSymbol.html
│   ├── TimeFrame.html
│   └── WebBot.html
├── LICENSE
├── package.yaml
├── README.md
├── Setup.hs
├── src
│   ├── Aggregate.hs
│   ├── BrokerBackend.hs
│   ├── Conf.hs
│   ├── GenBroker.hs
│   ├── Lib.hs
│   ├── MatsuiCoJp
│   │   ├── Broker.hs
│   │   ├── Model.hs
│   │   └── Scraper.hs
│   ├── ModelDef.hs
│   ├── Model.hs
│   ├── Scheduling.hs
│   ├── SinkSlack.hs
│   ├── StockQuotesCrawler.hs
│   └── TechnicalIndicators.hs
├── stack.yaml
├── stock-charts.ipynb
├── test
│   ├── 01k-db.com_indices_I101_1h.utf8.html
│   ├── 02k-db.com_indices_I101.utf8.html
│   ├── 03k-db.com_stocks_7312-T.utf8.html
│   ├── 04k-db.com_stocks_8306-T_1h.utf8.html
│   ├── ConfSpec.hs
│   ├── conf.test.json
│   ├── MatsuiCoJp
│   │   ├── 01www.deal.matsui.co.jp_servlet_ITS_asset_MoneyToSpare.utf8.html
│   │   ├── 01www.deal.matsui.co.jp_servlet_ITS_home_Announce.utf8.html
│   │   ├── 01www.deal.matsui.co.jp_servlet_ITS_stock_StkHavingList.utf8.html
│   │   ├── 02www.deal.matsui.co.jp_servlet_ITS_stock_StkHavingList.utf8.html
│   │   ├── BrokerSpec.hs
│   │   └── ScraperSpec.hs
│   ├── Spec.hs
│   └── StockQuotesCrawlerSpec.hs
└── tractor.cabal

6 directories, 80 files

設定ファイルを用意する

上にある過去記事を見に行ってください。

設定ファイルを編集する。

$ cat /src/Conf.hs

最後にあるこの部分を編集あるいはlogginguserを用意する。

-- |
-- ログデーターベース情報
loggingConnInfo :: MySQL.ConnectInfo
loggingConnInfo =
    MySQL.defaultConnectInfo
        { MySQL.connectUser = "logginguser"
        , MySQL.connectPassword = "loggingpassword"
        , MySQL.connectDatabase = "stockdb"
        }

stackでcompile

$ stack build

初回はghcのインストールから始まるので、かなり時間がかかる。終わるまで他のことでもしていましょう。(stack setupはもういらないはず。)

テストケースの実行

こいつ test/MatsuiCoJp/BrokerSpec.hs だけは松井証券にログインして売り注文を送る危険なテストなので、中身を確認してください。(分からなければこのファイルを test/ 以下ではない場所に移すか、削除してください。)

証券コード
9399 新華ホールディングス・リミテッド
売り株数
1
売り値段
189.0

以上の内容(ここに書いてある内容)で松井証券に発注します。

内容が確認できたら

$ stack test

でテストケースの実行をしてみてください。

実行

$ stack install
$ tractor

これで平日はSlackにメッセージを送ってきます。
ログアウトしてもtmuxのセッションが残るので、実行したままになります。

確認

RLoginでポートフォワードしてHeidiSQLで見る方法がここのページにあるので、その通りにしてください。

アクセスログからプログラムの動作を確認する

それなりの時間動かしているところに、データーベースにこのようなクエリを投げると

SELECT CONVERT_TZ(`received_at`,'+00:00', '+09:00') AS JST, `id`,  `received_at`,  `url`, `scheme`, `user_info`,  `host`,  `port`,  `path`, `query`, `fragment`, `req_cookie`, `req_header`, `resp_status`, `resp_version`, `resp_header`, `resp_cookie` `resp_body` FROM `stockdb`.`access_log` LIMIT 1000;

このとおり。

2017-12-19 06:30:00 AM,JST

正確なスケジューラを用意したので、ここに書いてある通り正確に06:30:00 AM,JSTにログインページにアクセスして、その結果をログに入れたのが 2017-12-19 06:30:00.137074 AM,JSTであって、サーバーからログインページを受け取っている事が、resp_bodyを右クリックして”ファイルに保存”を選んでそのファイルを見ると確認できる。

続けて、HTTP POSTメソッドでログイン情報を送って
(なんでPOST?と思ったら、さっき保存したファイルに form name=”form” method=”post”と書いてあるのを確認してみて。)
、結果をログに入れたのが06:30:00.219467 AM,JST この間 0.219467 – 0.137074 = 0.082393 [秒] = 82.393 [ミリ秒] でログインを済ませてサーバーとの間でセッションを確立している。

これ以降フレームを分解しながら、GM→LM→CTの順番でアクセスしながら目的のページを取り出す。

ここでは menuParam=ANNOUNCE つまりお知らせページを受け取る。
実際にはこの関数が呼ばれているので、この後資産評価を受け取ってデーターベースに入れている。

作業の最後にログアウトしたのが 2017-12-19 06:30:00.650603 AM,JST でこの間 0.650603 – 0.137074 = 0.513529 [秒] = 513.529 [ミリ秒] で終了した。(1秒以下どころか0.5秒位で終わらせるとは、超早いな)
ということがログから分かる。

2017-12-19 08:57:00 AM,JST

ここに書いたとおり作業は9:00:00 AM,JSTから始めるのだけれど、ログインは3分前に済ましておく仕掛けのために 2017-12-19 08:57:00.159163 AM,JST にセッションを確立している。

2017-12-19 09:00:00 AM,JST

これ以降この関数を設定ファイルのupdatePriceMinutesに書いてある時間で呼ぶ。(今回は10分なのでログにも9:00:00の次の作業が9:10:00であることが分かる)
この後、前場終了までセッションは維持したまま(ログアウトが2017-12-19 11:31:01.283414 AM,JST)で有ることがログを見れば分かる。

正確なスケジューラを用意しました。

予定時間と現在時間の差を確認しながら適度な時間でスレッドを停止しながら実行する仕掛け。特にここにある通り、過ぎた作業はキャンセルする仕掛けが大事かな。

失敗後の再復帰をかえました。

前のバージョンは失敗して例外が発生した時から一定時間待機後再復帰の構造だったわけだけど、時間がずれるのは具合が悪いので、今回から前半(0分から30分)と後半(30分から60分)の二つの枠に分けて、前半で例外が発生したら、後半で再復帰。あるいはその反対で動くようにしました。

つまりここのこと

立会時間(前場、後場)の時間を秒単位のリストにする関数によって手に入れた作業時間を(30*60)秒=30分毎にふるい分けして作ったリスト

[9:00:00, 9:30:00, 10:00:00, 10:30:00, 11:00:00, 11:30:00, 12:30:00, 13:00:00, 13:30:00, 14:00:00, 14:30:00, 15:00:00]

と関数とのタプルをスケジューラに渡してある。(この時間で再復帰を試みる)

この仕掛けは通常リストの先頭の関数が1日の作業を受け持つのですが、関数が例外を送出して終了すると次の関数が作業を再開するという感じ。

上に書いたとおり実行時間の過ぎたスケジュールはキャンセルされるし、プログラムに変数とか配列とかのややこしい物はないのでそれらの中身を気にせず外部からの呼び出しで再復帰が可能。(リストの全てが同じ関数に同じ入力であるから、その能力と権限は同じですからね)

たとえると前任が例外という名の辞世の句を詠んで果てると、後任が次の実行時間から再開するということですが、前任があえなく果てた所に即、後任を送るのは人道?にもとので、事態が落ち着いてから後任が作業を始める仕組み。

余談ですけど、
送電線の再閉路システムを思い出したりして。こんなやつ 再閉路方式 – コトバンク
事故発生時に該当する事故相を選択遮断した後、無電圧時間中の絶縁回復を期待して再閉路するやり方。関係ないですけどね。

平日と土日曜日とを分けました。

今日は何曜日?と確認して土日曜日は休日アクション、休日は株価の監視をしない。

k-db.comが

前回株価更新と集計処理を作ったときに、株価のダウンロードに使わせていただいた、k-db.comが2017年12月末でサービス終了とのようで。

株価ダウンロード、集計、テクニカル指標計算バッチ処理

-- |
-- バッチ処理関数
batchProcessing :: Conf.Info -> IO ()
batchProcessing conf =
    webCrawling <> aggregate $$ sink
    where
    --
    --
    sink =
        Slack.simpleTextMsg (Conf.slack conf)
        =$ Slack.sink (Conf.slack conf)
    -- |
    -- Webクローリング
    webCrawling = Q.runWebCrawlingPortfolios conf
    -- |
    -- 集計
    aggregate = Aggregate.runAggregateOfPortfolios conf

気に入っていたのに残念です。特にここ。

batchProcessing conf =
    webCrawling <> aggregate $$ sink

Webクローリング、その後集計。それらの出力はSlackに送る。という動作をこの1行で宣言的に表現しているところがお気に入り。

短い間でしたがこれまでありがとうございました。来年この部分はコメントアウトになる予定で、テストケースにその痕跡が残るんだろうな。

ちなみにテストケース

test以下に用意している。

プログラム書くでしょ

$ stack test
または
$ stack test --fast

って事を5分に一回くらいはしている(BrokerSpec.hsは削除しているから。迷惑だからね。)ので、テスト実行回数はすごいと思う。このテストケースはデーターベースから取り出してUTF8にした後、個人情報を消して使っている。

では今後

買い注文を実装して、売買双方向運転をしてみようと思う。(9399 新華ホールディングス・リミテッドなら誤発注しても耐えられそうだし)
そろそろSBI証券も実装しようかな。自分。このとおりやり方知ってますし。

python3でSBI証券にログインして発注する

コメントを残す

メールアドレスが公開されることはありません。