【完結】株の売買システムを自動的に実行する[4/4]

エクセルVBAとマーケットスピード2/RSSで株を自動売買する件
前回までのエントリで、マケスピ2とエクセルを起動する部品を作った。

あとは順次呼び出して完成だぜって話

では早速ビルド

例によって下記に貼りつけたコードをVSのプロジェクトに追加する。

次に環境依存の修正を加える。オープンしたい(VBAが入っている)エクセルのファイル名、マーケットスピード2ディレクトリへのパス、ログインパスワード、東証の休業日を修正してビルドする。ちなみに実行時刻は安心のハードコーディングなのでお好みにより適宜修正してくださいな。

PCの設定

あまり追求してないんだけど、画面ロックの状態ではUI Automationが動作しない。制限というか仕様なんだと思う。

なので、パスワードロック・スリープ移行・ディスプレイ電源オフ・スクリーンセーバーといった自動で画面をロックする機能をオフに設定すること

使い方

ビルドしてできた実行ファイルをエクセルファイルと同じディレクトリに置き、Power ShellとかCommand Prompt から実行する

時刻を判断して内部ステートが遷移し、マケスピ2起動、エクセルオープン、VBA実行、マケスピ2終了、を繰り返します。

ここで、何時に動かし始めるか?が問題になる

15:50~翌朝07:45 の時間帯に動かし始めればいいんだけど、それ以外の時間帯に起動したい場合、手動でマケスピ2やエクセル、VBAの起動状態を内部ステートと一致させておく必要がある。 

それと、エクセルを保存して終了するのはVBA側でやる必要があります。エクセル側の処理が終わったかどうかはエクセル側しか知らんから。。

cmdで動かす

まとめと残課題

一カ月くらい動かしてみた結果、デキはまあまあといったとこかな。ただし汎用性や信頼性って観点だと、諸々の条件次第で挙動が変わりそーな雰囲気がアリアリ。

そのあたりは利用環境に応じて調整するしかない。ウェイト値とか。

なお把握している問題は2つあって、Windows Updateが夜中にリブートして翌朝動いてないのと、たまーにマケスピがサーバ切断で落ちて復帰しないのがある。どっちも大勢に影響ないから放置。

その他雑感

今回楽天RSSで自動売買システムを自作した感想としては、結構イケるじゃん?と思った。

そう感じた理由は、個人でも手数料無料の機械売買ができる時代なんだなってのを実感したから。

テクニカルとか指標でシグナルを捻り出す系の自動売買ではなく、もっと単純にトレード回数を稼いでエッジを生み出せるじゃね?なんて思えた。

そうなると色々アルゴ考えて試してみるんだけど・・・・・・・・・・・・・・・・・

まあ、いい道具があったところでド素人の思い付きアルゴとか全く役に立たない、という原則を念のため再確認してるところです・笑😇

// Copyright 2022 maxsan
// Licensed under the Apache License, Version 2.0
using System;
using System.Threading;
using System.Windows.Forms.VisualStyles;

namespace TradeAutomation
{
    static class Constants
    {
        public const string VbaBookName = @"EXCEL_WORKBOOK.xlsm";		// workbook(xlsm) to be opened.
        public const string VbaMacroShortcutKey = @"^m";				// shortcut key to launch VBA
        public const string VbaMacroButtonName = @"app";				// 
        public const string MarketSpeed2FilePath = @"C:\Users\HOGE\AppData\Local\MarketSpeed2\Bin\"; // path to MarketSpeed2
        public const string MarketSpeed2PassWord = "login-password";	// login password
        public static readonly string[] holidays =
        {
            // 土日以外の東証休業日を列挙
            "2022/09/19", "2022/09/23",
            "2022/10/10", "2022/11/03", "2022/11/23", "2022/12/31"
        };
    }

    internal class Program
    {
        enum State { s1, s2, s3, s4, s5 };
        static void Main(string[] args)
        {
            MarketSpeed2Lib ms2 = new MarketSpeed2Lib();
            ExcelLib excel = new ExcelLib();
            DateTime prevDt = new DateTime();
            State state = State.s1;

            Disp("-------------------------------");
            Disp("RSS trade control v0.3.5 @221103");

            // 初期ステート設定
            // いつでも起動できるようにするのが目的だが
            // 開始ステートに応じた状態にしておくのは手動
            // マケスピ起動とかエクセル起動とか
            if (IsNowOrAfterTime("09:00"))
            {
                if (IsNowOrAfterTime("15:50"))
                {
                    Disp("初期ステート: s1");
                    state = State.s1;
                }
                else
                {
                    Disp("初期ステート: s4");
                    state = State.s4;
                }
            }

            // Main loop
            while (true)
            {
                DateTime dtNow = DateTime.Now;

                // 休業日
                if (IsHolidayJPX())
                {
                    if (prevDt.Date != dtNow.Date)
                    {
                        Disp("--------------------");
                        Disp("休業日");
                        Disp("ステート: s1");
                        state = State.s1;
                    }
                    prevDt = dtNow;
                    Thread.Sleep(1000);
                    AliveNotify();
                    continue;
                }

                // State1
                if (state == State.s1)
                {
                    if (IsNowTime("07:45"))
                    {
                        // 処理開始
                        Disp("--------------------");
                        Disp("処理開始");

                        // Market Speed II 起動
                        Disp("マケスピ2起動");
                        ms2.filepath = Constants.MarketSpeed2FilePath;
                        ms2.passwd = Constants.MarketSpeed2PassWord;
                        if (ms2.LaunchAndLogin() != 0)
                        {
                            Disp("マケスピ2起動失敗");
                            break;
                        }
                        ms2.MinimizeMainWindow();

                        state = State.s2;
                    }
                }

                // State2
                if (state == State.s2)
                {
                    //Thread.Sleep(10 * 1000);
                    if (IsNowTime("08:30"))
                    {
                        // エクセル起動
                        Disp("エクセル起動");
                        excel.bookname = Constants.VbaBookName;
                        if (excel.Launch() != 0)
                        {
                            Disp("エクセル起動失敗");
                            break;
                        }
                        // VBA起動
                        Disp("VBA実行");
                        excel.macroName = Constants.VbaMacroButtonName;
                        excel.StartMacro();

                        state = State.s3;
                    }
                }

                // State3
                if (state == State.s3)
                {
                    //Thread.Sleep(10 * 1000);
                    if (IsNowTime("08:40"))
                    {
                        // RSS接続、発注許可
                        Disp("RSS接続発注許可");
                        int i = 0;
                        while (excel.RssConnectAndEnableOrder() != 0)
                        {
                            Thread.Sleep(1000);
                            if (i++ > 100)
                            {
                                Disp("RSS接続発注許可失敗");
                                break;
                            }
                        }

                        state = State.s4;
                    }
                }

                // State4
                if (state == State.s4)
                {
#if false           // 要因により回復できないから無効化
                    // RSS接続状態チェックと回復
                    if (ms2.IsExistPopUpWindow() != 0)
                    {
                        // ポップアップがあればサーバ接続断と推定してOKを押下
                        // なければマケスピを閉じる
                        if (ms2.ClickOKonPopupWindow() != 0) ms2.CloseMainWindow();

                        ms2.LaunchAndLogin();
                        ms2.MinimizeMainWindow();

                        // RSS回復
                        while (excel.RssConnectAndEnableOrder() != 0) ;
                    }
#endif

                    //Thread.Sleep(30 * 1000);
                    if (IsNowTime("15:50"))
                    {
                        // Market Speed II クローズ
                        Disp("マケスピ2クローズ");
                        ms2.CloseMainWindow();
                        Disp("処理終了");

                        state = State.s1;
                    }
                }
                Thread.Sleep(1000);
                AliveNotify();
            }
            Disp("異常終了");
            while (true) Thread.Sleep(1000);
            //return;
        }

        /// <summary>
        /// 東証が休みか否か
        /// </summary>
        /// <returns></returns>
        private static bool IsHolidayJPX()
        {
            DateTime dtNow = DateTime.Now;

            // 休日?
            bool isHoliday = false;
            foreach (var day in Constants.holidays)
            {
                DateTime d = DateTime.Parse(day);
                if (d.Date == dtNow.Date) isHoliday = true;
            }

            // 土日?
            if ((dtNow.DayOfWeek == DayOfWeek.Saturday) ||
                (dtNow.DayOfWeek == DayOfWeek.Sunday)) isHoliday = true;

            return isHoliday;
        }

        /// <summary>
        /// 現在時刻を分単位で比較(到来チェック)
        /// </summary>
        /// <param name="hhmm"></param>
        /// <returns></returns>
        private static bool IsNowTime(string hhmm)
        {
            return (DateTime.Now.TimeOfDay.Ticks / TimeSpan.TicksPerMinute
                     == DateTime.Parse(hhmm).TimeOfDay.Ticks / TimeSpan.TicksPerMinute);
        }

        /// <summary>
        /// 現在時刻を分単位で比較(経過チェック)
        /// </summary>
        /// <param name="hhmm"></param>
        /// <returns></returns>
        private static bool IsNowOrAfterTime(string hhmm)
        {
            return (DateTime.Now.TimeOfDay.Ticks / TimeSpan.TicksPerMinute
                     >= DateTime.Parse(hhmm).TimeOfDay.Ticks / TimeSpan.TicksPerMinute);
        }

        /// <summary>
        /// コンソール出力(日時つき)
        /// </summary>
        /// <param name="msg"></param>
        private static void Disp(string msg)
        {
            Console.WriteLine(DateTime.Now + " " + msg);
        }

        static string[] pgchr = { "[O  ]", "[ o ]", "[  O]", "[ o ]" };
        static int cntr = 0;
        /// <summary>
        /// So called 'Spinning Cursor'
        /// </summary>
        private static void AliveNotify()
        {
            Console.Write(pgchr[cntr++ % 4]);
            Console.SetCursorPosition(0, Console.CursorTop);
        }
    }
}