見出し画像

株式投資でプログラミング!パート❺ ー ”新値買い"のカップパターンはこれだ!勝率が高いのは「60日カップの20日保有」で勝率は・・・

前回のPart 4ではApple株を例に、「60日カップ、40日保有」というルールをみていきました(”カップ”についてはPart 1より記事を参照してください)。つまり、「新値更新まで最低3か月をかけたカップで、新値で買いを入れてから2か月は保有している」という条件で投資した場合、Appleは7勝1敗。そしてMicrosoft(MSFT)だと9戦全勝という結果となりました。

カップの最長期間も設定してみる

ところが、Googleを調べると8勝5敗、Amazonに至っては7勝5敗とうれしくない結果となっています。ということは、たまたまアップルとマイクロ路ソフトがよかっただけの話なのでしょうか。

そこで結果をよ~く観察してみると(”人間AI”です)、どうも結果に疑問が湧いてきます。例えばAmazonのこのカップ例を見てください。

ID:0
Starting High: 88.089996
Starting Date: 2008/08/11
New High: 88.230003
New Date: 2009/07/20
Bottom Price: 34.68
Cup Length: 235
Hyp Gain Rate: -7.979146277485665

これは新値買いでマイナスになったケースですが、「カップ期間が235日」という点に注目してください。これは前にも述べましたが、営業日で235日ということ。つまり1か月20日として単純計算で12か月、つまり1年間も新値更新していなかったケースです。一年かけてようやく新値を更新したパターンというのはどうなのでしょうか。確かにそこから新たなストーリが展開されるのかもしれませんが、あまりに長期すぎます。そこで、カップの最大期間というのも設定してみます

Part 4で書いたコードのforeachループの箇所だけ示しています。とりあえずここではカップの最長を120日、つまり6か月としてみます。変更箇所は3行です。


//カップ情報のパラメータ
int HoldLength = 40;
int minCup = 60;
int maxCup = 120; = 

foreach (var pricetoday in prices)
{
   //もし今日の株価が過去最高より高かったらカップチェック
   if (pricetoday.Close > HighPrice)
   {
       //もしX日より長く、Y日より短かいならカップ!
       if (CupDays > minCup && CupDays < 120)
       {
           //カップ情報をひょじする
           Console.WriteLine("Found a cup!!");
           Console.WriteLine("Starting High: " + HighPrice.ToString());
           Console.WriteLine("Starting Date: " + HighDate.ToShortDateString());
           Console.WriteLine("New High: " + pricetoday.Close.ToString());
           Console.WriteLine("New Date: " + pricetoday.Date.ToShortDateString());
           Console.WriteLine("Bottom Price: " + LowPrice.ToString());
           Console.WriteLine("Bottom Day: " + LowDate.ToShortDateString());
           Console.WriteLine("Cup Length: " + CupDays.ToString());
           double hypGain = GetHypGain(pricetoday.Date, pricetoday.Close, HoldLength, prices);
           double hypGainRate = hypGain / pricetoday.Close * 100;
           Console.WriteLine("Hyp Gain Rate: " + hypGainRate.ToString());
           Console.WriteLine();
       }

       //リセット
       HighPrice = pricetoday.Close;
       HighDate = pricetoday.Date;
       LowPrice = 1000000;
       LowDate = pricetoday.Date;
       CupDays = 0;
   }

まずカップの最短と最長の期間をminCupmaxCupという変数名にしました。

int minCup = 60;
int maxCup = 120; 

そしてそれを条件に入れます。

//もしX日より長く、Y日より短かいならカップ!
if (CupDays > minCup && CupDays < 120)

これで勝率を計算するとなんと!

● MSFT(マイクロソフト)4戦全勝
● AAPL(アップル)4戦全勝
● AMZN(アマゾン)4戦全勝
● GOOG(グーグル)6勝2敗

カップのケースは少なくなりましたが、勝率は格段に上がりました。つまり、AAPL、AMZN、MSFTに限って言えば「3~6か月以内に新高値を更新した場合、そこで買いを入れて2か月後に売れば儲かる」ということになります。ますまず怪しい話になってきましたが、ここはあくまでプログラミングのアプローチの話なのでこうした結果はどうでもよいのですが、ストーリーはちょっと先走るくらいのほうが作業も面白くなるでしょう!

画像1

最適のカップ取引条件を見つける!

カップの条件はとりあえず3~6か月にしてみるとして、「2か月で売る」という保有期間はどうなのでしょう。もっと短期で売ったほうが儲かる確率が上がるかもしれませんし、逆に長く持っていたほうがよいかもしれません。これを調べつために日数を変えて何度もプログラムを走らせるのは面倒です。そこで、これもループを使ってコードにやらせてみまよう。そこで最適な保有期間が見つかるかもしれません。

何をやるかというと、保有期間を例えば20日(1か月)から始めて、5日(1週間)づつ増やしていき、120日間まですべて調べてみます。つまりプログラムを20回(120日-20日÷5)走らせるということです。しかもそれをAAPLなど4つの銘柄でやるわけですから合計80回。そんなの手作業で一つひとつやっていては大変ですよね。ここはプログラムを大改良して、いくつの銘柄でも、どんな期間でもすべて計算するようにします

Yahoo Finance APIの結果をキャッシュする

カップの計算は2008年以降過去12年ものデータをYahoo Finance APIから取得して行います。何度も何度もAPIコールを行うとサービスに負荷がかかります。過去のデータは一度取得すると一切変更はありません。そこで、一度取得したデータはローカルのファイルに保存しておき、2度目以降はそこからデータを取るようにします。そうするとAPIコールは銘柄数だけで済みます。

コードがとても長くなるので、まずは完成形のYahooFinaceAPI.csのファイルをGithubにアップしておきました。このコードを皆さんのコードにコピペして使ってください。全コピペで構いません。

ここではデータのキャッシュをどうやるかについて大筋を解説するにとどめます。まずキャッシュのロジックはこんな感じです。

❶ GetStockPricesメソッドを呼び出すと、APIを呼び出す前にまずは”ローカルキャッシュファイル”があるかどうかをチェックする。
  ● 例えばAAPLの場合、AAPL.txtなどのファイルにデータ保存されるようにしておけば、そのファイルがあるかないかでキャッシュされていたかどうかを簡単に確認できる。
● もしキャッシュがなければ、それは一番最初のコールなのでAPIを呼び出す。
● もしキャッシュがあればローカルファイルからデータを読み込む。
❷ キャッシュに保存するSaveStockPricesInFileメソッドを書く。
❸ キャッシュからデータを読むGetStockPricesFromFileメソッドを書く。

基本この3つのメソッド(既存メソッド変更が一つ、新しいメソッドが2つ)でキャッシュは完成します。

詳しいところはコードをじっくり読んでみてください。ここで特殊なことをやっているのはキャッシュファイルのネーミングです。

///データをファイルに書き出す
File.WriteAllText(@"StockData\" + symbol + "_" 
 + FromDate.ToShortDateString().Replace(@"/", "") + "_" 
 + ToDate.ToShortDateString().Replace(@"/", "") + ".txt", s
 b.ToString(), Encoding.UTF8);

これはファイルに読み込んだ過去の株価データを書き込むコードですが、ファイル名をStockDataというフォルダの中に、例えば"AAPL_172008_172020.txt"のような名前で保存します。同じ銘柄でも取得期間が変わればデータも変わってきます。銘柄+期間でユニークな名前にすることができます。これで一度APIを呼び出して取得したデータはすべて以下のようなファイルにキャッシュされ、条件が同じであれば二回目以降はすべてこのローカルファイルから読み込まれることになります。あとは何度呼び出しても誰にも迷惑はかけません!

画像2

カップ取得の関数を作る

さて、過去の株価取得はこれで何度も呼び出せるようになりましたが、そこからカップを取得するのも何回もやることになります。今のコードではMainメソッドにどかーんとカップ取得のロジックが入ってしまっています。そこでカップ取得のメソッドも関数としてまとめてみます。

       /// <summary>
       /// カップ取得メソッド
       /// </summary>
       /// <param name="symbol"></param>
       /// <param name="cupduration"></param>
       /// <param name="buyduration"></param>
       /// <returns></returns>
       private static List<Cup> GetCups(List<HistoryPrice> prices, int minCup, int maxCup)
       {
           double highPrice = 0.0;
           DateTime highDate = new DateTime();
           double lowPrice = 100000000.0;
           int cupcounter = 0;
           int cupdays = 0;
           List<Cup> cups = new List<Cup>();

           foreach (HistoryPrice pricetoday in prices)
           {
               //もし新高値更新の場合
               if (pricetoday.Close > highPrice)
               {
                   //もしカップ最低期日を満たしていたらカップ登録
                   if (cupdays > minCup && cupdays < maxCup)
                   {
                       //新カップ登録
                       var newcup = new Cup();
                       newcup.StartDate = highDate;
                       newcup.StartPrice = highPrice;
                       newcup.BottomPrice = lowPrice;
                       newcup.HighDate = pricetoday.Date;
                       newcup.HighPrice = pricetoday.Close;
                       newcup.CupDays = cupdays;
                       newcup.CupID = cupcounter++;

                       cups.Add(newcup);

                       //カップカウンターを
                       cupcounter++;
                   }

                   //リセット
                   highPrice = pricetoday.Close;
                   highDate = pricetoday.Date;
                   lowPrice = 1000000;
                   cupdays = 0;
               }
               else
               {
                   //最安値だったら記録
                   if (lowPrice > pricetoday.Low)
                   {
                       lowPrice = pricetoday.Low;
                   }
                   
                   //カップ期日計算をインクリメント
                   cupdays++;
               }
           }
           return cups;
       }

これはよくみるとMainで書いていたコードをほぼそのまま関数にした感じがわかると思います。ただし、一つ加えたのがCupというクラスを作ったことです。カップには始まった日にち、その時の株価、高値更新日、新高値などいろんな「要素」が入ってきます。それをCupというクラスで表現したのがこれです。

   /// <summary>
   /// Cupクラス
   /// </summary>
   class Cup
   {
       public int CupID; //カップの番号
       public int CupDays { get; set; } //カップの長さ
       public DateTime StartDate { get; set; } //カップ開始日
       public DateTime HighDate { get; set; } //カップ開始の株価
       public double StartPrice { get; set; } //高値更新日
       public double HighPrice { get; set; } //新高値
       public double BottomPrice { get; set; } //カップ期間中の最安値
   }

このように扱いたいデータにたくさんの要素がある場合はClassにまとめると複雑な構造のデータも簡単に扱うことができます。

勝率計算の関数を作る

ここでもう一つ関数を作ります。ある銘柄に対してカップを取得したら、普通は複数見つかります。そのカップ一つ一つに対して、「最高値更新日に株を買って、〇〇日後に売ったら何パーセント上がる・下がるか」を計算しないといけません。この「カップ取得」と「勝率計算」を一気に行うGetWinningRateという関数を作ります。

/// <summary>
/// 過去の株価からカップ分析して勝率を計算する
/// </summary>
/// <param name="prices">過去の株価データ</param>
/// <param name="minCup">カップ最低期間</param>
/// <param name="maxCup">カップ最長期間</param>
/// <param name="holdLength">保有期間</param>        /// <returns></returns>
private static double GetWinningRate (List<HistoryPrice> prices, int minCup, int maxCup, int holdLength)
{
   //まずはカップを取得する
   var cups = GetCups(prices, minCup, maxCup);

   //勝敗数の記録
   int win = 0;
   int lose = 0;

   //それぞれのカップの勝率を計算する
   foreach (var cup in cups)
   {
       //仮想株価上昇率を計算
       double hypGain = GetHypGain(cup.HighDate, cup.HighPrice, holdLength, prices);
       double hypGainRate = hypGain / cup.HighPrice * 100;
       //勝敗を記録
       if (hypGainRate >= 0) win++;
       else lose++;
   }
   //勝敗率を表示する
   Console.WriteLine("    " + win.ToString() + "/" + (win + lose).ToString());
   return (float)win / (win + lose);
}

この関数には「過去の株価データ」「カップの最短期間」「カップの最長期間」「株保有期間」という4つのパラメータを入れると勝率を計算してくれます。例えは「これがアップルの株価データ、カップは最低30日、最長120日、保有期間を40日で計算して、勝率を教えて」というと即計算してくれるというわけです。

中でやっていることは、さっき作ったGetCupメソッドでカップを取得し、与えられたカップ期間にもとづいて、新値買いの勝敗数(株価が上がったかどうか)を計算。勝敗計算ではすでに作っているGetHypGain関数を使います。実施に結構複雑なことをやっていますが、実質10行でこの関数が完成しているのはGetCupGetHypGainなどメソッドをすでに作っているからです。

Mainメソッドはこんなに簡単に!

それぞれの計算ツール(関数)ができたので、あとはそれを実行するだけです。Mainメソッドをまずはご覧ください。

static void Main(string[] args)
{
   //調べたい銘柄をリストする
   string[] stocks = new string[] { "MSFT", "AAPL", "GOOG", "AMZN" };

   //最高の勝率とその際の保有期間
   double BestRate = 0;
   double BestHold = 0;

   //保有期間を20日から120日まで5日ステップでチェックする
   for (int h = 20; h <= 120; h+=5)
   {
       Console.WriteLine("保有期間: " + h.ToString() + "日 ============");
       double totalRate = 0;

       //指定した銘柄をそれぞれカップ分析する
       foreach (var stock in stocks)
       {
           //過去の株価を取得
           List<HistoryPrice> history =
               YahooFinanceAPI.GetStockPrices(stock, new DateTime(2008, 7, 1), new DateTime(2020, 7, 1));
           //カップ分析+勝率の計算
           double rate = GetWinningRate(history, 60, 120, h);
           Console.WriteLine(stock + ": " + rate.ToString());
           totalRate += rate;
       }

       Console.WriteLine("勝率: " + (totalRate / stocks.Length).ToString() + "\n") ;

       //最高勝率をチェックする
       if (BestRate < totalRate)
       {
           BestRate = totalRate;
           BestHold = h;
       }
   }

   //最高勝率と保有期間の表示
   Console.WriteLine();
   Console.WriteLine("最適株保有期間: " + BestHold.ToString());
   Console.WriteLine("最高勝率: " + (BestRate / stocks.Length).ToString());

   Console.ReadLine();
}

これも全部で18行で、その多くが文字の表示に使われているので結構単純なコードになっています。

まずは調べたい銘柄を入れます。Ticker Symbolを文字列配列にいれます。ここではMSFT、AAPL、GOOG、AMZNの4つを入れています。

   //調べたい銘柄をリストする
   string[] stocks = new string[] { "MSFT", "AAPL", "GOOG", "AMZN" };

最初のループは保有期間のパターンです。

 //保有期間を20日から120日まで5日ステップでチェックする
 for (int h = 20; h <= 120; h+=5)
 }

ここでは保有期間をhという変数に入れますが、20から始めて120まで繰り返しますが。ただ各ループでhは5ずつ追加されるので20, 25, 30, 35, ...となります。つまり5日ごとでデータを見ていくということです。

それぞれの保有期間パターンで、4つの銘柄すべての勝敗を計算します。

 //指定した銘柄をそれぞれカップ分析する
 foreach (var stock in stocks)
  {

ここでループ内にループがある形になっています。

保有期間20のループ 
{
    銘柄4つのループ
   {
         計算処理
    }
}

これで銘柄4回のループが保有期間のループ20回が合計80回おこなわれるのがわかりますか。

なんだか頭が混乱してきたかもしれませんが、まずはプログラムを走らせてみましょう。Program.csファイルの中身が相当複雑になっているので、これもGitHubにアップロードしておきました。ここからコードをコピペするかダウンロードして使ってください。(注:ファイル名はProgram2.csですが、中身を皆さんのProgram.csにコピペしてください)

最高の勝率はなんと96%?!

とりあえずこのプログラムを走らせるとこうなりました。全ての結果は長いので途中省略してあります。最後の最高勝率とその時の株保有期間に注目してください。

保有期間: 20日 ============
   4/4
MSFT: 1
   4/4
AAPL: 1
   5/5
GOOG: 1
   7/8
AMZN: 0.875
勝率: 0.96875

保有期間: 25日 ============
   3/4
MSFT: 0.75
   3/4
AAPL: 0.75
   5/5
GOOG: 1
   5/8
AMZN: 0.625
勝率: 0.78125

保有期間: 30日 ============
   4/4
MSFT: 1
   4/4
AAPL: 1
   5/5
GOOG: 1
   6/8
AMZN: 0.75
勝率: 0.9375


<この中省略>

保有期間: 120日 ============
   4/4
MSFT: 1
   3/4
AAPL: 0.75
   4/5
GOOG: 0.800000011920929
   6/8
AMZN: 0.75
勝率: 0.8250000029802322


最適株保有期間: 20
最高勝率: 0.96875

これは20日間から5日ステップでみたなかで、4銘柄の勝敗率平均をそれぞれ計算してみた結果です。すると、この4銘柄では「保有期間20日で平均勝率は96%」とでてきました!

つまりこの大手IT銘柄でみると、新値買いで1か月で売れば儲かるということになるのです。「そんな短期でぶち売っていいの?」という感じですが、まあ単純アルゴリズムではそうなっているのです。なんだか面白い結果になりました。

プロジェクトのまとめ

とりあえず「株式でプログラミング」企画はここで終えます。もし興味がある方はこのアプリをもっと拡張してみてください。カップの期間やさらにカップ内での値動き、そして取引高なども加味するともっと勝率が高いカップパターンが見つかるかもしれません。勝率ではなく上昇率を見るという視点もあります。勝った!と言ってもたった数%の上昇ではつまらないですよね。こうやって興味を軸にいろいろとやってみると自然にコーディング技術がついてくるものです。

是非とも面白い自作アプリにチャレンジしてみて下さい!!





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