KEIS BLOGは株式会社ケイズ・ソフトウェアが運営しています。

KEIS BLOG

Splunkに株価を取り込んでみた ft. Fujikawa


はじめまして。本日、初めてブログを書きます、下田です。他の方は一体どんなブログを書いているのだろうと眺めていると、ふと目に留まった記事がありました。それは、「Splunkを使ってみよう!」という記事でした。

私は、Splunkを触ったことがないのですが、すごく簡単に分析が出来そうだったの興味を持ちました。何かSplunkを使って面白いことができないかなぁと考えた時に、ログとは時系列のデータなので「時系列のデータなら分析が得意そう!」と思い、個人で秘密裏に溜め込んでいた約1年分の日本の株データ(日単位の終値)を読み込ませたらどうなるんだろうという単純な好奇心のもとでこの記事を書き始めました。現在書いている時点では、上手く行くか全く検討がつきませんが、クソ記事だったとしても石を投げたくなる気持ちをグッと抑えて広い心で読んでいただければと思います。

まずは、秘密裏に保管している株データを取ってきます。

$ scp tac@example.jp:/var/stock/stockdata/*gz .

ちょっとどんな感じでデータを保管していたか覚えていないので、展開してみます。

$ gunzip stock2014-10-14.gz

何かすごくデカイファイルなので整形させて中を見てみます。

$ cat stock2014-10-14|python -m json.tool|less

どうやらこんな感じで入っていました。数字も文字列として入っています。非常に残念です。適当に書いて困るのはいつも自分なんですけどね。

{
    "JP:1301-TS": {
        "BPS": "187.57",
        "CapitalAdequacyRatio": "23.40%",
        "ChangeInSharesBoughtOnMargin": "-53,000",
        "ChangeInSharesSoldOnMargin": "-35,000",
        "CurrentRatio": "153.62%",
        "DebtEquityRatio": "211.50%",
        "DividendOnEquity": "2.63%",
        "DividendPayoutRatio": "17.70%",
        "DividendPerShare": "5",
        "DividendPerShareForecast": "5",
        "DividendYield": "1.83%",
        "DividendYieldForecast": "1.99%",
        "EarningsPerShare": "28",
        "Exchange": "\u6771\u8a3c\u4e00\u90e8",
        "FiscalHealthScore": "25",
        "GrowthScore": "63",
        "InventoryTurnoverRate": "6.46"
    }
}

こんな感じで全銘柄が約1年分あります。このデータを参考にしたブログ記事を元に整形していきたいと思います。
日付は、その日の15時(マーケットが閉まる時間)で、IP アドレスの部分はティッカーシンボルを、それ以降にその日の終値を入れてみようと思います。
この株データの整形は、手馴れているJavaを使って整形したいと思います。と言っても整形するのにすごく時間がかかりました・・・。

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;

import com.fasterxml.jackson.databind.ObjectMapper;

public class Transformer {
    public static void main(String[] args) {
        try {
            transform("./stockdata/", "./stock.log");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void transform(String from, String to) throws IOException {
        final ObjectMapper om = new ObjectMapper();
        final Pattern datePtn = Pattern.compile("([0-9]{4}-[0-9]{2}-[0-9]{2})");
        final Comparator<Path> comp = new Comparator<Path>() {
            @Override
            public int compare(Path o1, Path o2) {
                return o1.toFile().getName().compareTo(o2.toFile().getName());
            }
        };
        Files.list(Paths.get(from))
                .filter(f -> f.toFile().getName().endsWith(".gz"))
                .sorted(comp)
                .map(f -> {
                    Matcher matcher = datePtn.matcher(f.toFile().getName());
                    matcher.find();
                    return new StockFile(matcher.group(0), f);
                })
                .map(sf -> {
                    try (InputStream inputStream = new GZIPInputStream(Files.newInputStream(sf.path, StandardOpenOption.READ));) {
                        sf.data = om.readValue(inputStream, Map.class);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return sf;
                })
                .forEach(sf -> {
                    List<String> list = (List<String>) sf.data.keySet()
                            .stream().map(k -> {
                                Map object = (Map) sf.data.get(k);
                                String price = (String) object.get("price");
                                return String.format("%s 15:00:00.000\t%s\t%s", sf.date, k, price);
                            }).collect(Collectors.toList());
                    try {
                        Files.write(Paths.get(to), list, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
        System.out.println("Done");
    }

    static class StockFile {
        String date;
        Path path;
        Map data;

        public StockFile(String date, Path path) {
            super();
            this.date = date;
            this.path = path;
        }
    }
}

やっている内容はとても単純で、gzipを日付でソートして展開してJSONを読み込んで特定のカラムを引っ張ってきてファイルに書き込んでいます。冗長だと思った方、これがJavaの世界です(違います。)

2015-07-24 15:00:00.000     JP:9362-TS     181.0
2015-07-24 15:00:00.000     JP:8002-TS     671.0
2015-07-24 15:00:00.000     JP:4026-TS     600.0
2015-07-24 15:00:00.000     JP:8891-NG     null
2015-07-24 15:00:00.000     JP:1960-TS     null

こんなログが出来上がりました。(すごく取りこぼしている気がする。)
ファイルサイズは、30MB程度で、80万行あります。果たしていい感じにグラフが出るのでしょうか。

shimoda01

例によって、同じように、Field extractions にそれぞれ展開した値を設定します。今回は、日付、ティッカーシンボルと終値です。
まずは、東京電力を探してみます。東京電力のティッカーシンボルは、9501なので、以下のように見よう見まねで入れてみました。

index=* sourcetype=stockdata  | search stock_ticker=9501

shimoda02

何だかそれっぽいデータの羅列が見えてきました。それでは、それっぽいデータが取れたところで、Visualizationタブで時系列に可視化してみます。
先ほど利用したクエリにパイプで繋げて関数みたいなものを付けてみます。

index=* sourcetype=stockdata  | search stock_ticker=9501 | timechart avg(stock_price) by stock_ticker

shimoda03

おお、何だか様になってきました。もう少し調べてみるとこのチャート、複数のデータを載せることが出来るみたいです。早速やってみます。
東京電力と関係した会社といえば、関西電力です。今度は関西電力のティッカーシンボルを追加したいと思います。ちなみにティッカーシンボルは、9503です。先ほどのクエリに更に OR というオペレータを使ってOR条件を追加していきます。

index=* sourcetype=stockdata  | search stock_ticker=9501 OR stock_ticker=9503 | timechart avg(stock_price) by stock_ticker

shimoda04

・・・うーん、微妙ですね。この株価は一株あたりの終値を出しているのですが、一株あたりの値段の差が大きいとあまり違いが分かりにくいですね。しかし、このグラフから読めるのは、電力業界という括りで株価はゆったりと同調していると言っていいのではないでしょうか。今までこういった分析を個人がするには、随分と骨が折れる作業だったのですが、今ではログを取り込んで少し検索方法を学ぶだけであっという間に自分の欲しいグラフが作れるような時代になりました。最後は少し株の話にそれましたが、自宅に眠っている時系列のデータを弄くり倒して新たな知見が簡単に得られるようになったという結論で今回の話は終わりたいと思います。ありがとうございました。