見出し画像

シストレで使っている米国株の現在値をLINE通知するプログラム

実際に以下運用中のシステムトレードのロジックで毎日走らせているコードから抜粋してます。

「現在値の取得」と「LINE通知」の機能それぞれであれば、いくらでもサンプルコードがネットに散らばってると思います。車輪の再発明はしないがプログラマーの鉄則なので、このコードもそういったサンプルコード(やAI)を利用しています。

が、ナウなヤングの間ではPythonを使ったコードが多いことや、投資やシストレに特化した組み合わせはあまりないと思い、それであれば少しは付加価値があるかなと思いnoteにしました。

ちなみに各証券会社でも現在値等のアラートサービスはありますが、前後にロジックを入れて条件分岐させたり、他の情報を加えたり(私は注文数等も送ってます)といったカスタマイズの幅が広いので応用が効きますし、後述するサーバのスクショを送るとかもプログラミングならではです。

📈S&P 500の現在値をスクレイピングする

はじめに

投資データのスクレイピングといえばPythonのyfinanceライブラリがデファクトスタンダードになっていると思います(そしてサンプルコードはいくらでもあります)。が、ここではJavaのJsoupを使って実装しています。

自分用に作っているので汎用性は追求してませんが、米国版Yahoo! Financeで表示可能な銘柄(例えばティッカーシンボルがある米国株)であれば対応しています。

注意点

  • JsoupでHTMLのタグや属性値を使って取得していますが、Yahoo! Finance側の仕様変更で変わる可能性があるのでご注意ください。ここ1年ほどでも1回仕様変更があって修正対応しました。

  • それに関連して、異常値が返った場合の回避処理を受け取り側で実装してください。私はあらかじめの価格帯(S&P 500であれば5500とか)を登録しておいて、そこから2割以上ずれるようであればExceptionを投げるようにしています。

  • なお、Pythonのyfinanceでも同じですが、同一IPから短期間に大量のアクセスをするとYahoo側からIPブロックされると思いますし、場合によっては違法になる可能性もあるのでご注意ください。

    私は日次の指標チェックとして使っているので日に数回ほどですが、大量の過去データやリアルタイムデータが欲しい場合はIB TWS APIのreqHistoricalTicksや、reqTickByTickDataを使うことをお勧めします。

    なお、IB TWS APIであれば株と先物は250ミリ秒、オプションは100ミリ秒、FXは5ミリ秒ごとに更新されるのでAPI経由の方がレイテンシーの面でも有利です。

使い方

簡単な使い方は以下の2つです。

  • getSpot(ティッカーシンボル):銘柄の現在値(Spot price)が取れます。例えばgetSpot("AAPL")でアップルの株価233.85を返します。

  • getChange(ティッカーシンボル):銘柄の前日比を返します。getChange("AAPL")で2.55を返します。

私はインデックスの価格チェックに使っているので、Yahoo! Financeの命名規則に従ってインデックスや先物の表記に合わせるように修正するメソッドをgetIndexSpot(String), getFutureSpot(String)で実装しています。対応しているのはS&P 500、Russell 2000、Nasdaqのみですが、必要があればgetFutureNameの中を修正してください。

getOvernightIndex(String)は少々特殊で、「寄付価格を予測する」メソッドです。インデックス自体は米国東部時間16時に閉まりますが、24時間動いている先物中心限月の前日比を調整することで、オーバーナイトでの変化を吸収するようにしています。特に寄り付き前に走らると直前までの先物変化を反映できます。

Bloomberg等を見ている方であれば、Pre-Open時間帯に先物の動きを報道しているシーンをご存知だと思いますがそれに近いものをコード化した感じになります。

細かくやるならSQ日に合わせたロールオーバー時の対応等も入れた方がいいのでしょうが、注文用のロジックに直接使っているわけではないので割り切ってます。

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

public class YahooJsoup {

    public static void main(String[] args) throws IOException, InterruptedException {
        // 現在の価格を表示する
        String name = "SPX"; //"NDX" <- Nasdaqの場合はこちら;
        System.out.println("現在値:" + getOvernightIndex(name));
        System.out.println("前日比:" + getChange(getFutureName(name)));
    }

    public static double getOvernightIndex(String name) throws IOException, InterruptedException {
        double lastPrice = getIndexSpot(name);
        Thread.sleep(1000);

        String futureName = getFutureName(name);
        double esChange = getChange(futureName);
        return lastPrice + esChange;
    }

    private static String getFutureName(String indexName) {
        switch (indexName) {
            case "SPX":
                return "ES=F";
            case "RUT":
                return "RTY=F";
            case "NDX":
                return "NQ=F";
        }
        return null;
    }

    public static double getIndexSpot(String symbolCode) throws IOException {
        return getSpot("^" + symbolCode);
    }
    public static double getFutureSpot(String symbolCode) throws IOException {
        return getSpot(symbolCode + "=F");
    }
    public static double getSpot(String symbolCode) throws IOException {
        Document document = getDocument(symbolCode);

        String cssQuery = "fin-streamer[data-field='regularMarketPrice']";
        Element priceElement = document.select(cssQuery).first();

        double price = 0;
        // 要素が存在する場合のみ、テキストを取得する
        if (priceElement != null) {
            String priceString = priceElement.attr("data-value");
            price = Double.parseDouble(priceString);
        }
        return price;
    }

    public static double getChange(String symbolCode) throws IOException {
        if (symbolCode == null || symbolCode.equals("")) return 0;
        Document document = getDocument(symbolCode);

        // 現在の価格を取得する
        String cssQuery = "fin-streamer[data-field='regularMarketChange']";
        Element priceElement = document.select(cssQuery).first();
        double price = 0;
        // 要素が存在する場合のみ、テキストを取得する
        if (priceElement != null) {
            String priceString = priceElement.attr("data-value");
            price = Double.parseDouble(priceString);
        }
        return price;
    }

    private static Document getDocument(String symbolCode) throws IOException {
        return Jsoup.connect("https://finance.yahoo.com/quote/" + parseToUrlEncode(symbolCode)).get();
    }

    public static String parseToUrlEncode (String str) throws UnsupportedEncodingException {
            return URLEncoder.encode(str, "UTF-8");
    }
}

📲LINEを使ってシストレ経過を送る

はじめに

以下のnoteでも触れましたが、同じようなことは20年以上前にもやっていて、当時はJ-Phoneのスカイメールで、早稲田の自宅で走らせているPCからシグナルが点灯した銘柄コードやBid, Ask情報を錦糸町で働いている自分に送っていました。

今では伝達手段もメール、テキストメッセージ、メッセンジャーアプリと多様化しているので選択肢が増えた分APIも便利になって実装しやくなったと思います。

注意点

noteにしておいてなんですが、LINEの公式ページによるとLINE Notify の機能は2025年3月末で終了だそうです。

とはいえ、次の代替サービスが準備されているそうなので、実装方法は似たような形になるのかと思います。が、有料になる可能性もあり今後のアップデートをチェックしていきたいです。まあ、WhatsAppやTelegramでも同様のAPIはあるのでいざとなったらWhatsApp版を作ればいいかなと思います。

トークンの発行や初期設定等、詳しくは公式ページをご参照ください。

使い方

とりあえずトークン等の初期設定を行います。

初期設定時に受け取るメッセージ

notify("送りたい文字列")で送信できます。以下のコードのmainメソッドではJsoupで取得したSPXの価格を送信しています。実行すると以下のような形でLINE Notify からメッセージを受け取れます。

ちなみに画像やスタンプも送れるようですが以下のコードでは実装してません。シストレには文字列で十分だと思いますが、外出中に実行環境の状況を確認したい場合にデスクトップのスクリーンショットを送るといった使い道もあると思います。

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.stream.Collectors;

public class LineMessenger {
    private static final String TOKEN = "自分のトークン文字列を入力";

    public static void main(String[] args) throws Exception {
        double spxSpotPrice = YahooJsoup.getIndexSpot("SPX");
        LineMessenger.notify("SPXの価格は$" + spxSpotPrice);
    }

    public static void notify(String message) {
        HttpURLConnection connection = null;
        try {
            URL url = new URL("https://notify-api.line.me/api/notify");
            connection = (HttpURLConnection) url.openConnection();
            connection.setDoOutput(true);
            connection.setRequestMethod("POST");
            connection.addRequestProperty("Authorization", "Bearer " + TOKEN);
            try (OutputStream os = connection.getOutputStream();
                 PrintWriter writer = new PrintWriter(os)) {
                writer.append("message=").append(URLEncoder.encode(message, "UTF-8")).flush();
                try (InputStream is = connection.getInputStream();
                    BufferedReader r = new BufferedReader(new InputStreamReader(is))) {
                    String res = r.lines().collect(Collectors.joining());
                    if (!res.contains("\"message\":\"ok\"")) {
                        System.out.println(res);
                    }
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
    }
}

最後にお約束ですが、上記サンプルコードは仕様の変更により意図しない結果が返る場合があるのでご自分の環境で十分テストされた上で自己責任でお願いします🙇‍♂️

この記事が気に入ったらサポートをしてみませんか?