株式投資でプログラミング!パート❸ ー ”新値買いパターン”をプログラミングで検知する方法を、文系でもわかるように解説!
今回のPart 3はちょっとしたアルゴリズムとデータ処理の世界に足を踏み入れます。TOEIC英単語アプリでもそうでしたが、これまでは基本的にはクラスや関数を呼び出す、ある意味レゴブロックを組み合わせるような作業がほとんどでしたが、今回は「これぞプログラミング」というようなロジックについて解説します。新値買いのタイミングとなる「カップ」は一定のルールをもとに株価推移のパターンであり、そのパターンを見つけるのがロジックです。この「カップパターン」のロジックの大まかな流れについてはPart 1で概要を解説していますのでそちらを参考にしてください。
まずはアップルの過去1年の株を見てみよう
ここまででYahoo FinanceのAPIが使えるようになっているので(詳しくはPart 2を見てください)、まずは特定銘柄に絞って株価を見ていきます。そこでApple(Symbol:AAPL)の過去一年の株価を取得して、それを一日一日見ていきましょう。
Part 2で作ったコンソールアプリ、CupAndHandleのMainメソッドを空にして、次のようなコードから始めます。実質加えているのは2行です。
using System;
namespace CupAndHandle
{
class Program
{
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
foreach (var pricetoday in prices)
{
//ここにカップ検知のコードを書く
}
Console.ReadLine();
}
}
}
最初のGetStockPricesはPart 2で解説した通りですが、ここではアップルのTicker Symbol、AAPLを指定、期間は2019年の7月1日から2020年の同日までの一年としています。その結果がpricesというList<HistoryPrice>型の変数に入ります。そしてその株価リストをforeachでズズーっとスキャンしながら読んでいくわけです。つまりそのループの中でカップパターンをチェックするコードを書いていくことになります。
ロジックの中心は「今日の株価は最高値更新か?」
もう一度「カップ」の形を思い出してください。
今回のスキャンでは最初は19年の7月1日が起点です。例えば上の図で青線のカップの左端、つまり起点が7月1日だと考えてください。今回のデータは19年7月1日からデータが始まるので、その時点までの本当の最高値はわかりません。なので、まずスタート地点での株価を最高値とします。そこから次の日の株価をみて、次のような判断をします。
【カップチェックの視点】
❶ もし今日の株価が現行最高値よりも低ければ最高値は維持
❷ もし今日の株価が現行最高値よりも高ければ最高値を更新
要は、毎日の株価を見ていって、それがそれまでの最高値より上かどうかを見るだけなのです。なぜなら、青線のカップが完成するのは、7月1日の起点最高値よりも上を記録し、その時点でカップの右側が出来上がったということだからです。なので、最高値を更新しない限りどんどんと毎日の株価を読み込んでいきます。つまりforeachループによる株価スキャンが続くということです。
ではこのカップチェック、つまり最高値更新チェックをコードにするとこんな感じになります。IF文が入ったところを見てください。
using System;
namespace CupAndHandle
{
class Program
{
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
foreach (var pricetoday in prices)
{
//高値更新したら
if (pricetoday.Close > 最高値)
{
//これがカップパターンなのでカップ処理をする
}
//高値更新ではないなら
else
{
//まだカップの中なので次に進む
}
}
Console.ReadLine();
}
}
}
pricetodayはforeachで出てくる毎日の株価データ(型はHistoryPriceです)で、pricetoday.Closeは終値です。HistoryPriceはほかにも始値(Open)や高値(High)などがありますが、カップチェックではとりあえず引けの値段だけを見ていきます。それを「最高値」と比べています。
//高値更新したら
if (pricetoday.Close > 最高値)
えっ?”最高値”って?そう、これはあくまで「概念」であって、コードではありません。「最高値」という変数みたいなものがあればいいな、という感じでとりあえず書いているだけです。
そこで実際の最高値ってどこに記録されているのでしょうか。これは日々刻々と変わるかもしれませんよね。ループを進むごとにその時点での過去最高値を記録しておかなければいけません。ではそれを入れておく変数をまず作ってやりましょう。
そこでHighPriceというdouble型の変数を定義します。さらにその日付も記録しておきたいのでHighDateというDateTime型の変数も用意します。数字はこれまで整数のintを使ってきましたが、株価は小数点以下がたくさん入ってくるのでここはdoubleという型を使います。DateTimeは文字通り日時を扱うデータです。
次のコードを見てください。どの2行が追加されたかわかりますか?
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
double HighPrice = 0;//とりえあえず0に設定
DateTime HighDate = new DateTime(2019, 7, 1);
foreach (var pricetoday in prices)
{
//高値更新したら
if (pricetoday.Close > HighPrice)
{
//これがカップかもしれないのでカップ処理をする
}
//高値更新ではない
else
{
//まだカップの中なので次に進む
}
}
Console.ReadLine();
}
注:Mainメソッドだけ表示
視点となるデータを記録しておく変数をループ前に作っておくのがポイント
まずループが始まる前にこの2つの変数を用意している(宣言している)ことに注目してください。起点での過去最高値は分からないので、最高値はとりあえず0に設定しておきます。一方、日付は起点の7月1日を入れておきます。
これから長いループ処理の中で、常にこの「現時点での最高値」をチェックし続けます。こうした”視点”となるデータをループ前に変数として宣言しておくのが一つのポイントです。
ではとりあえずの最高値とその日付を記録しておき、いざループが始まります。そこでループの第一日目で何が起きるか皆さんわかりますか?
カップの”期間”が決め手となる
そうです、高値は即更新です!
最初に初期化した最高値が0ですから当然ですよね。初日にどんな値段が来てもそれが新高値になります。でも当然これはカップではありません。なぜか?それはたった一日という短い期間ではカップは成立しないからです。この”小カップ現象”はこの先も何度も出てくるはずです。株価がちょっと好調ならすぐに高値を更新します。でもそれはカップではありません。なぜなら、「カップには一定の期間が必要だから」です。もう一度説明しますが、カップというのは、ある高値からズルズルと値を下げていき、ある期間底でジクジクと低迷して推移したあと、スルスルと値を上げていき、最終的には起点の最高値を上回るという現象です。そんな短期のV字回復みたいなのはカップとはみなしません。
ではその「カップ期間」って一体どれくらいなんでしょうか?
実はわかりません!というのは、これからそれを見つけていかないといけないからです。そこで今はとりあえず30日にしましょう。つまり、最高値を更新しても30日以内の短期ではカップとはみなさないというルールを設定します。
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
double HighPrice = 0;//とりえあえず0に設定
DateTime HighDate = new DateTime(2019, 7, 1);
foreach (var pricetoday in prices)
{
//高値更新したら
if (pricetoday.Close > HighPrice)
{
//カップかどうかの判断をする
if (カップの期間 > 30)
{
//カップ発見!!
}
else
{
//カップではない!
}
}
//高値更新ではない
else
{
//まだカップの中なので次に進む
}
}
Console.ReadLine();
}
カップの期間 > 30 のところを見てください。もし今読んでいる株価が過去の最高値を上回った場合、それは即カップということになりません。今は一時的に決めた「カップは最低30日」というルールがあるので、それも判断しないといけません。最高値を更新しない限り毎日の株価を読み続けるので、その読み込んできた「期間」というのがありますよね。つまり「最高値更新しない連続日数」ということです。これが「カップの期間ということになります。それが30日より長かったらおめでとうございます!それがカップです。でも30日より短かったらそれをカップとはしないのでパスします。
それぞれの処理は後で書きますが、ここではその「カップの期間」というのはどこに入っているのかを考えます。毎日の株価を読み込んでいって、最高値を更新しない限りどんとんと読み込んでいきます。その際に読み込んだ日数を数えて納めておく変数を作ります。これもHighPriceやHighDateと同様、ループ前に用意しておきます。それをCupDaysとしましょう。
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
double HighPrice = 0;//とりえあえず0に設定
DateTime HighDate = new DateTime(2019, 7, 1);
int CupDays = 0; //カップの長さを記録する
foreach (var pricetoday in prices)
{
//高値更新したら
if (pricetoday.Close > HighPrice)
{
//カップかどうかの判断をする
if (CupDays > 30)
{
//カップ発見!!
}
else
{
//カップではない!
}
}
//高値更新ではない
else
{
//まだカップの中なので次に進む
}
//CupDaysカウンタを一つ上げる
CupDays++;
}
Console.ReadLine();
}
CupDaysは最初は0にしておきます。
int CupDays = 0; //カップの長さを記録する
そしてこれにどうやってカウントした日数を入れるかというと、それはこの行を見てください。
//CupDaysカウンタを一つ上げる
CupDays++;
++というのはその整数の変数に1を足すということです。これはforeachforeachループの最後に入っているので、ループが進む(次の日にデータに移動する)たびに1つ増えることになります。まさに日数カウンターの役割を果たします。
だいぶ話が複雑になってきた感があるのでループの中での処理の流れをいったんまとめます。
【ループ内でのカップチェック処理】
❶ もしその日の株価が最高値更新なら、
<カップかどうかチェックする>
① もし更新までの期間が30日未満ならカップではない。次に進む。
② もし更新までの期間が30日以上ならそれはカップ。
❷ それ以外、つまり株価が最高値よりも下だったら、
次に進む。
このチェック作業を毎日の株価に対して実行しているところを想像してください。まず見ているのは「その日の株価が最高値を更新したかどうか」。もしそうなら「カップ資格の30日を超えているかどうか」をチェック。基本はこの2点だけなのです。
そこで高値更新時点で「カップだった場合」、「カップではなかった場合」の2つの条件でどのようなコーディングをするかをそれぞれみていきます。
条件1:もしカップが発見されたらどうする?
では最初にこの”本丸”である「カップが見つかった」ケースでどんなコーディングをするか解説します。まずカップが見つかったら何をしたいかですが、今のところはそのカップ情報をコンソール画面に表示させるようにします。つまり、カップは何日間づづいたか、最高値はどのくらいか、最高値の日はいつだったかなどをチェックできるようにします。
ただし、カップ情報を画面に表示された後、一つ大事な作業があります。とりあえずその時点でカップパターンが見つかったので、それれ自体は終わりですが、ループによるスキャンは続き、この先にまたあらたなカップパターンが現れるかもしれません。なので、それぞれの「記録変数」をリセットしておかないといけません。具体的には、
● 最高値のHighPriceはその日の値(つまり新最高値)を入れておく
● 最高値日付もその日の日付で更新
● カップ期間のカウントも次のカップのために初期値に戻すので0にする
それを踏まえて次のコードを見てください。最初のIF文のブロックに新しいコードが入っています。
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
double HighPrice = 0;//とりえあえず0に設定
DateTime HighDate = new DateTime(2019, 7, 1);
int CupDays = 0; //カップの長さを記録する
foreach (var pricetoday in prices)
{
//高値更新したら
if (pricetoday.Close > HighPrice)
{
//カップかどうかの判断をする
if (CupDays > 30)
{
//カップ情報をひょじする
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("Cup Length: " + CupDays.ToString());
Console.WriteLine(); //表示で一行空ける
//記録変数をリセット
HighPrice = pricetoday.Close;
HighDate = pricetoday.Date;
CupDays = 0;
}
else
{
//カップではない!
}
}
//高値更新ではない
else
{
//まだカップの中なので次に進む
}
//CupDaysカウンタを一つ上げる
CupDays++;
}
Console.ReadLine();
}
まずカップ情報の表示部分ですが、カップが見つかると”Found a Cup!"という文字を表示し、新旧の最高値とその日付、そしてカップ期間が何日だったかもわかるようにします。これは分かりますよね。
その上で、もうカップは検知されたので、次のカップチェックのために3つの記録用変数をリセットします。
条件2:もしカップではなかったら
最高値更新でも期間が短いとカップになりません。この場合は処理としてはスルーするだけですが、肝心なのは最高値や最高値日付をリセットしないといけないことです。カップではなかったにしても最高値は更新されたので、これから見る高値はこれまでの最高値ではなく、今日更新された新たな最高値です。そしてもう一つ、カップ期間のカウンターも0にもどしておきます。
それを踏まえて二つ目のIF文を書いてみます。
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
double HighPrice = 0;//とりえあえず0に設定
DateTime HighDate = new DateTime(2019, 7, 1);
int CupDays = 0; //カップの長さを記録する
foreach (var pricetoday in prices)
{
//高値更新したら
if (pricetoday.Close > HighPrice)
{
//カップかどうかの判断をする
if (CupDays > 30)
{
//カップ情報をひょじする
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("Cup Length: " + CupDays.ToString());
Console.WriteLine();
//リセット
HighPrice = pricetoday.Close;
HighDate = pricetoday.Date;
CupDays = 0;
}
else
{
//今日の株価を最高値としてリセットする
HighPrice = pricetoday.Close;
HighDate = pricetoday.Date;
CupDays = 0;
}
}
//高値更新ではない
else
{
//まだカップの中なので次に進む
}
//CupDaysカウンタを一つ上げる
CupDays++;
}
Console.ReadLine();
}
さて、ここでちょっとコードのクリーンアップです。このIF文で変数リセット部分が重複していませんか?
//カップかどうかの判断をする
if (CupDays > 30)
{
//カップ情報をひょじする
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("Cup Length: " + CupDays.ToString());
Console.WriteLine();
//リセット
HighPrice = pricetoday.Close;
HighDate = pricetoday.Date;
CupDays = 0;
}
else
{
//今日の株価を最高値としてリセットする
HighPrice = pricetoday.Close;
HighDate = pricetoday.Date;
CupDays = 0;
}
カップ期間が30日以上でも以下でも、いずれにしても変数のリセット必要なのです。であればif (CupDays > 30)をチェックした後、普通にリセットすればよいだけです。結局、30日を超えていなかったらスルーするだけなのでelseの部分は必要ないということです。
//カップかどうかの判断をする
//もし30日を超えていたらカップ検知!なので情報を表示
if (CupDays > 30)
{
//カップ情報をひょじする
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("Cup Length: " + CupDays.ToString());
Console.WriteLine();
}
//記録変数をリセット
HighPrice = pricetoday.Close;
HighDate = pricetoday.Date;
CupDays = 0;
ただ、いきなりこう書くと、細かい条件文の流れがわかりにくくなるので、まずは全体の流れをすべて説明してきました。結果的に条件処理を簡素化することができただけの話ですので、なぜこうなったかはあまり気にしないでください。
条件3:今日の株価は高値更新ではない
これまでは、今日の株価が過去の高値を更新した場合を考えてきましたが、その反対で今日の株価は最高値より下であるケースがあります。その場合はもちろん何も処理は発生せずにスルーなのです。だとするとここもelseの部分は不要となるのですが、ここでは一つ操作を加えます。
何かと言うと、まずカップの左側でズルズルと株価が下がっていく局面で「底の値段」というのもチェックしておきたいです。つまりそれはカップの「深さ」を調べるためです。もしかするとカップの底の深さによって、カップ後の上昇具合が変わってくるかもしれません、確証はありませんが、こういったデータは多いほうが分析には便利です。
そこでLowPriceとLowDateというデータも追加してみます。HighPriceとHighDateの対のデータです。もし今日の株価が最高値より下であっても、それがこれまでの「最安値」よりも低いかどうかを調べます。もし下だったら値を更新します。
そこでLowPriceとLowDateの変数も含め、3つ目の条件のコードを入れたものがこれです。
static void Main(string[] args)
{
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2019, 7, 1), new DateTime(2020, 7, 1));
//====================
//株価を一日づつ見ていく
//====================
double HighPrice = 0;//とりえあえず0に設定
DateTime HighDate = new DateTime(2019, 7, 1);
double LowPrice = 1000000; //とりあえず百万ドルに設定
DateTime LowDate = new DateTime(2019, 7, 1);
int CupDays = 0; //カップの長さを記録する
foreach (var pricetoday in prices)
{
//高値更新したら
if (pricetoday.Close > HighPrice)
{
//カップかどうかの判断をする
//もし30日を超えていたらカップ検知!なので情報を表示
if (CupDays > 30)
{
//カップ情報をひょじする
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());
Console.WriteLine();
}
//記録用の変数リセット
HighPrice = pricetoday.Close;
HighDate = pricetoday.Date;
LowPrice = 1000000;
LowDate = pricetoday.Date;
CupDays = 0;
}
//高値更新ではない
else
{
//最安値チェック
if (LowPrice > pricetoday.Close)
{
LowPrice = pricetoday.Close;
LowDate = pricetoday.Date;
}
}
//CupDaysカウンタを一つ上げる
CupDays++;
}
Console.ReadLine();
}
まずLowPriceの最初の値を1000000、つまり100万ドルにしている点に注目してください。これをHighPriceと同様0にしてしまうと一生更新されません。当然ですが0より安い株はありませんので、これは最高値とは状況が違います。一番簡単なのは考えられないくらい高い値で初期化しておくと、最初の日の値段は確実にそれよりも低いので最安値をうまく更新できます。100マンドルとしたのはそのためです。
一方、条件1のところにLowPriceとLowDateのリセット行が入っているのでそこも注意してください。
長~い解説についてきていただきありがとうございます。これで最初の一歩は完成です!ではアプリをスタートさせてアップルの株価でカップはあるのかどうか調べてみましょう!
過去一年でのアップルの株価推移で”カップ現象”は1回、しかもつい最近!
アプリを立ち上げると次のような結果がすぐに出てきます。
Found a cup!!
Starting High: 327.200012
Starting Date: 2020/02/12
New High: 331.5
New Date: 2020/06/05
Bottom Price: 193.339996
Bottom Day: 2019/08/05
Cup Length: 79
今年2月に$327だったのを最後に6月までの79日間、一度も高値は更新されず、その間の底値は$193だったというカップパターンです。まあ当然ですがこれはコロナショックによるものですよね。そうです、3月の株価暴落でカップの底を這い、最近新値を更新したという、まさにカップ現象が起きていたのです!
でもカップは一回だけ?ではちょっと期間を延ばして、2017年から見ていきましょう。GetStockPricesのパラメータを変えるだけです。
//過去の株価を取得
var prices = YahooFinanceAPI.GetStockPrices("AAPL", new DateTime(2017, 7, 1), new DateTime(2020, 7, 1));
するともっとカップパターンが出てきました。全部で6つです。
Found a cup!!
Starting High: 164.050003
Starting Date: 2017/09/01
New High: 166.720001
New Date: 2017/10/30
Bottom Price: 142.729996
Bottom Day: 2017/07/06
Cup Length: 40
Found a cup!!
Starting High: 179.259995
Starting Date: 2018/01/18
New High: 179.979996
New Date: 2018/03/09
Bottom Price: 155.149994
Bottom Day: 2018/02/08
Cup Length: 35
Found a cup!!
Starting High: 181.720001
Starting Date: 2018/03/12
New High: 183.830002
New Date: 2018/05/04
Bottom Price: 162.320007
Bottom Day: 2018/04/27
Cup Length: 38
Found a cup!!
Starting High: 193.979996
Starting Date: 2018/06/06
New High: 194.820007
New Date: 2018/07/25
Bottom Price: 182.169998
Bottom Day: 2018/06/25
Cup Length: 34
Found a cup!!
Starting High: 232.070007
Starting Date: 2018/10/03
New High: 236.210007
New Date: 2019/10/11
Bottom Price: 142.190002
Bottom Day: 2019/01/03
Cup Length: 257
Found a cup!!
Starting High: 327.200012
Starting Date: 2020/02/12
New High: 331.5
New Date: 2020/06/05
Bottom Price: 224.369995
Bottom Day: 2020/03/23
Cup Length: 79
34日という短いのもあれば、257日という特大のカップもあります。何だか面白い結果が出てきました。
プログラミングとしては作業は大変でしたし、ロジックもそれなりに複雑でした。でも結果を見ても「ふ~ん」って感じですよね。カップが6つ見つかったのはよかったものの、「だから何なの?」という点がはっきりしません。そもそもカップパターンを見つけるのは、カップの後に株価が上昇するという”定説”があるからです。カップを見つけただけでは話は終わりません。
そこで次回は、この見つかったカップパターンの後に株価を買ったら本当にもうかっていたのかという点を検証していきましょう。
次回Part 4では
「もしカップパターンの新値更新で株を買ったらどれだけもうかるのか」という点についてコーディングを進めていきます。ただ、今のところカップの定義は30日以上ということで、とりあえずのスペックです。そもそもこれも正しいかどうか不明です。そこで次のような目標を立ててみましょう。、
「ある一定のカップパターンの後で株を買い、〇〇日間保有して売ったら8割以上の確率で勝つ(=儲けが出る)」
「新値は買い」ということなら、カップパターンで株を買うと10回中8回くらいは儲けさせてもらわないと困ります。ただ、カップ期間だけではなくカップの深さや取引高などもかかわっているかもしれません。こうした「黄金のカップパターン」が見つかるかどうかは別として、とにかくプログラミングの勉強としてやってみましょう!
もしそんなパターンがあるなら、今そのパターンになっている銘柄が見つかれば・・・などと色気が湧いてきますが、あくまで本記事はプログラミングの勉強であって投資の指南ではありません。お間違いのないように!
では乞うご期待!
この記事が気に入ったらサポートをしてみませんか?