[入門編]C#で株APIを呼び出し、予算内で購入出来る人気の株を取得してみた

#はじめに

この記事はN・S高等学校Advent Calendar 2021の20日目の記事です。

筆者は[N Code Labo](https://n-codelabo.jp/)の中の人をしています。
年々N・S高等学校Advent Calendarに参加してくれるN Code Labo生が増えており、とても嬉しく思います。
数年後、N Code Labo単独でアドベントカレンダーが完走出来たら良いなぁと夢見ています。
※以下二つが生徒の記事です。

https://qiita.com/Ame-ba/items/96d9d282b552c5737882

https://qiita.com/so1bloom/items/72ebb6119b06ff3c2507


本題に入ります。
早速ですが、皆様は株式投資に興味はありますか？

N・S高等学校には[投資部](https://nnn.ed.jp/about/club/investment/)という取り組みがあり、角川ドワンゴ学園から提供される運用資金を元手に、株式投資に挑戦します。
投資部という響きから、お金持ちを目指す・お金を増やす事が目的の部活という印象を与えがちですが、リンク先のHPに記載がある通り、**投資を通じて社会や経済の仕組みを実践的に学ぶ事**を目的としている部活動です。
投資と聞くと、あまりクリーンなイメージを持つことが難しいと思います。ですが、きちんと学ぶと社会で生きていく上で基礎的な力を育む事が出来ますので、食わず嫌いでいるのは勿体ないと感じます。
本記事を通じて、一人でも投資に興味を持ってくれるエンジニアが増えたら幸いです。

※本記事は投資を推奨する物ではございません。

#環境

kabu STATION API:https://kabu.com/company/lp/lp90.html
→auカブコム証券様が提供している個人投資家向け株取引APIです。利用するに辺り、auカブコム証券様の取引口座及びkabuステーションの準備が必要となります。
詳細はこちらをご覧ください。https://kabucom.github.io/kabusapi/ptal/howto.html#step1

Visual Studio 2019(.Net 4.6を指定したコンソールアプリケーションを作成いただければ、記事のコードを試す事が出来ます)

#実現したいこと

日々のデイトレードを助けてくれる機能を作ります。
具体的には以下です。

1. ランキングで盛り上がっている業種を取得する。
※kabuステーションでは、画像の通り値上がりしている業種のランキングを取得する事が出来ます。
![ranking.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36448/e2096543-304e-1b3f-c33f-ead3324eaa1a.jpeg)


2. 盛り上がっている業種に属している銘柄の情報を取得する。

2. 取得した銘柄の中から、予算内で購入できる銘柄を抽出する。

それでは、マニュアルを読みながら進めていきます。

#実装を試みる

##公式が提供するサンプルコードを確認する

公式が[サンプル](https://kabucom.github.io/kabusapi/ptal/index.html)を公開してくれているので、そちらからC#用をダウンロードします。
C#以外にもPython・JavaScript・Excelがありとても親切です。
サンプルを開くと、認証から各APIのコールの仕方まで書いてあります。日頃REST APIの実行になれていない人でも、ここまで用意されていれば手を出しやすいと感じます。
まずはランキングデータを取得する所から始めますので、Kabusapi_Ranking.csの実装を参考にします。

##業種別ランキングを取得する

マニュアルは[こちら](https://kabucom.github.io/kabusapi/reference/index.html#operation/rankingGet)です。
必須パラメーターは「Type」と「ExchangeDivision」の二つと分かりましたので、
Typeに14(業種別値上がり率)を、ExchangeDivisionにALL(全市場)を指定すれば欲しいデータが取得出来そうです。
早速、サンプルのコードを丸コピし、以下コードを試してみます。
※kabuステーションを起動していないと、エラーになるので注意。

```csharp:GetRanking.cs
		static void Main(string[] args)
		{
			// APIトークンを発行
			string token = GenerateToken.GetToken();

			// 業種別値上がりランキングを全市場で取得
			string Type = "14";
			string ExchangeDivision = "ALL";
			var builder = new UriBuilder("http://localhost:18080/kabusapi/ranking");
			var param = System.Web.HttpUtility.ParseQueryString(builder.Query);
			param["type"] = Type;
			param["exchange"] = ExchangeDivision;

			builder.Query = param.ToString();
			string url = builder.ToString();
			try
			{
				// HTTPClientを用意し、requestを作成
				var client = new HttpClient();
				var request = new HttpRequestMessage(HttpMethod.Get, url);
				request.Headers.Add("X-API-KEY", token);
				// requestを送り、responseを取得
				HttpResponseMessage response = client.SendAsync(request).Result;
				// Consoleに結果を表示
				Console.WriteLine("{0} \n {1}", JsonConvert.DeserializeObject(response.Content.ReadAsStringAsync().Result), response.Headers);
			}
			catch (HttpRequestException e)
			{
				Console.WriteLine("{0} {1}", e, e.Message);
				Console.ReadKey();
			}
			catch (Exception ex)
			{
				Console.WriteLine("{0} {1}", ex, ex.Message);
				Console.ReadKey();
			}

			Console.ReadKey();
		}

```

こちらが実行結果であり、きちんと結果が取れていそうです!!幸先が良いですね。
![ranking_result.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36448/c28bad45-e22e-610b-e84c-5f45317967b2.jpeg)

JSONを扱いやすくするために、入れ物となるクラスを作成します。
Visual Sutdioには、JSON文字列をクラスにして張り付けてくれる機能があるので、とても簡単に作れます。
![json_generator.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36448/6a074b84-6067-f948-6834-261a6f2d12db.jpeg)
以下の様なクラスを生成してくれます。

```csharp:RankingObject.cs
	public class RankingObjext
	{
		public int Type { get; set; }
		public string ExchangeDivision { get; set; }
		public Ranking[] Ranking { get; set; }
	}

	public class Ranking
	{
		public int No { get; set; }
		public object Trend { get; set; }
		public double AverageRanking { get; set; }
		public string Category { get; set; }
		public string CategoryName { get; set; }
		public float CurrentPrice { get; set; }
		public float ChangeRatio { get; set; }
		public string CurrentPriceTime { get; set; }
		public float ChangePercentage { get; set; }
	}
```

**注意!!公式マニュアルに記載されているJSONを元に生成すると、型が違うエラーが発生します!!responseデータから生成する様にしましょう。**

下記画像は、OverSellQtyはdouble型でresponseが返ってくると記載されているにも関わらず、response例にint型が記載されてしまっている例です。
response例にある数値は基本全てintになってしまっている様なのでご注意ください。

![json_trap.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36448/ba99b979-7843-f60a-dc14-c9bc300105ea.jpeg)

JSON形式からクラスにする事で、非常に扱いやすくなります。やっぱり型は最高ですね。
![ranking_debug.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36448/29f302b8-0610-2c3a-9136-faeedac96331.jpeg)

無事、業種ランキングが取得出来ましたので、業種に紐づく銘柄の株価を取得します。

##業種に紐づく銘柄の株価を取得する

時価情報を取得するためにマニュアルを見ていたら、銘柄コード@市場コードの形式でリクエストを送る必要がある事が分かりました。
![symbol_manual.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36448/738d955e-9e47-7fa3-d7e0-63cab79d72bc.jpeg)
※例えば銘柄コード9468の、東証に上場している銘柄の情報が欲しければ、9468@1と指定する必要があります。

私が見落としているだけかもしれませんが、残念ながら業種に属する銘柄を検索する事は出来ない様です。。。
仕方がないので、設定ファイルで書く形式にします。設定ファイルは以下の様な、
keyに業種コード、valueに業種に紐づく銘柄をカンマ区切りで記載する方式で書きます。
※好みが出る所なので、あくまで参考程度に捉えていただけますと幸いです。
※業種に紐づく銘柄は、「サービス業　銘柄」的なワードで検索すると[情報サイト](https://minkabu.jp/stock/stocksitemap/33)を見つける事が出来ます。

```csharp:App.config
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup> 
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
  </startup>
  <appSettings>
    <add key="322" value="1515,1518" /> <!--鉱業業-->
    <add key="338" value="4543,5187" /> <!--精密業-->
    <add key="342" value="9101,9104" /> <!--海運業-->
    <add key="345" value="1973,2307" /> <!--情報・通信業-->
    <add key="346" value="1352,2667" /> <!--卸売り業-->
    <add key="353" value="2121,4751" /> <!--サービス業-->
  </appSettings>
</configuration>
```

設定ファイルから値を取得する例です。詳しく知りたい方は以下ドキュメントをご確認ください。
https://docs.microsoft.com/ja-jp/dotnet/api/system.configuration.configurationmanager


```csharp:UseConfig.cs
				// 設定ファイルから、業種コードをkeyに、業種に紐づく銘柄コードリストをvalueにした辞書型の変数を生成する
				var dicCategory = new Dictionary<string, List<string>>();
				// 全てのkeyとvalueを取得する例
				foreach (string key in ConfigurationManager.AppSettings.AllKeys)
				{
					// カンマ区切りで分割
					var values = ConfigurationManager.AppSettings[key].Split(',');
					foreach (string value in values)
					{
						if (dicCategory.ContainsKey(key))
						{
							dicCategory[key].Add(value);
						}
						else
						{
							dicCategory.Add(key, new List<string>() { value });
						}
					}
				}
```

LINQを使うと、以下の様に設定ファイルに記載した銘柄コード全てをListに纏める事も出来ます。
tips的な立ち位置で載せておきます。

```csharp:Tips.cs
// 二次元配列にした後に、1次元配列にする。
var allSymbol = ConfigurationManager.AppSettings.AllKeys.Select(x => ConfigurationManager.AppSettings[x].Split(',')).SelectMany(x => x).ToList();
```

設定ファイルから業種に紐づく銘柄を取得する事が出来るようになりましたので、実際に株価を取得しようと思います。
topCategoryListに業種コードに紐づく銘柄のリストを格納し、LINQを使ってAPIリクエストURLを銘柄ごとに生成しています。
※時価情報APIのresponseを受け取るために、SymbolObjectクラスを生成しています。

```csharp:GetBoard.cs
				string Exchange = "1";
				var topCategoryList = dicCategory[ranking[0].Category];
				var urlList = topCategoryList.Select(symbol => "http://localhost:18080/kabusapi/board" + "/" + symbol + "@" + Exchange).ToList();

				foreach (var requestURL in urlList)
				{
					var board_request = new HttpRequestMessage(HttpMethod.Get, requestURL);
					board_request.Headers.Add("X-API-KEY", token);
					HttpResponseMessage board_response = client.SendAsync(board_request).Result;
					var symbol = JsonConvert.DeserializeObject<SymbolObject>(board_response.Content.ReadAsStringAsync().Result);
					Console.WriteLine($"銘柄名：{symbol.SymbolName} 1株価格：{symbol.CurrentPrice}");
				}

```

実行結果です。銘柄名と株価が取得出来ています。
![board_result.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36448/9e8a484d-6ea3-9b51-735b-3472b47e182e.jpeg)

##コードの全体像

コードの全体像は以下です。ところどころ行儀が悪い個所がありますが、ご容赦いただけますと幸いです。
APIを実行する上で、C#が最適な最適解かと言われると議論の余地は大いにありますが、検討先としては悪くないのではないかと感じます。
やろうと思えばUnityから株取引も出来ますし。


```csharp:GetBoard.cs
		static void Main(string[] args)
		{
			// APIトークンを発行
			string token = GenerateToken.GetToken();

			// 業種別値上がりランキングを全市場で取得
			string Type = "14";
			string ExchangeDivision = "ALL";
			var builder = new UriBuilder("http://localhost:18080/kabusapi/ranking");
			var param = System.Web.HttpUtility.ParseQueryString(builder.Query);
			param["type"] = Type;
			param["exchange"] = ExchangeDivision;

			builder.Query = param.ToString();
			string url = builder.ToString();
			try
			{
				// HTTPClientを用意し、requestを作成
				var client = new HttpClient();
				var request = new HttpRequestMessage(HttpMethod.Get, url);
				request.Headers.Add("X-API-KEY", token);
				// requestを送り、responseを取得
				HttpResponseMessage response = client.SendAsync(request).Result;
				var ranking = JsonConvert.DeserializeObject<RankingObjext>(response.Content.ReadAsStringAsync().Result).Ranking.ToList();

				// 設定ファイルから、業種コードをkeyに、業種に紐づく銘柄コードリストをvalueにした辞書型の変数を生成する
				var dicCategory = new Dictionary<string, List<string>>();
				// 全てのkeyとvalueを取得する例
				foreach (string key in ConfigurationManager.AppSettings.AllKeys)
				{
					// カンマ区切りで分割
					var values = ConfigurationManager.AppSettings[key].Split(',');
					foreach (string value in values)
					{
						if (dicCategory.ContainsKey(key))
						{
							dicCategory[key].Add(value);
						}
						else
						{
							dicCategory.Add(key, new List<string>() { value });
						}
					}
				}

				// 時価情報を取得するためのリクエストURLを生成
				string Exchange = "1";
				var topCategoryList = dicCategory[ranking[0].Category];
				var urlList = topCategoryList.Select(symbol => "http://localhost:18080/kabusapi/board" + "/" + symbol + "@" + Exchange).ToList();
				// 予算額を一旦30万円に設定
				int budget = 300000;
				foreach (var requestURL in urlList)
				{
					var board_request = new HttpRequestMessage(HttpMethod.Get, requestURL);
					board_request.Headers.Add("X-API-KEY", token);
					HttpResponseMessage board_response = client.SendAsync(board_request).Result;
					var symbol = JsonConvert.DeserializeObject<SymbolObject>(board_response.Content.ReadAsStringAsync().Result);

					// 実行した結果、予算よりも安ければコンソールに出力
					var price = symbol.CurrentPrice * 100;
					if (price < budget)
					{

						Console.WriteLine($"予算内銘柄：{symbol.SymbolName} 1単元価格：{price}");
					}
				}
			}
			catch (HttpRequestException e)
			{
				Console.WriteLine("{0} {1}", e, e.Message);
				Console.ReadKey();
			}
			catch (Exception ex)
			{
				Console.WriteLine("{0} {1}", ex, ex.Message);
				Console.ReadKey();
			}

			Console.ReadKey();
		}
```

#まとめ

今回は情報系と呼ばれる株価情報を取得するAPIを試しました。
株価情報の取得を試みるだけでも、世の中にはどれくらいの数の業種が存在しているのか？や自分の身の回りの企業は上場しているのか？値上がりしているのか？など様々な事に興味を持つことが出来ますので、非常に勉強になると思います。
また、真剣に自動売買ツールを実装しようとなったら、REST APIでは秒間10件程度しか取得出来ないため、Push APIの利用方法を学ばなければいけなかったりと、**やりたいをきっかけにどんどん学ぶ事が増え、気づいたら凄いレベルまで到達している**という理想的な成長体験が得られると思っています。
※リクエスト制約はこちらのページに記載があります：https://kabucom.github.io/kabusapi/reference/index.html#tag/info

N Code Labo・N/S中・高生とはSlackなどで繋がっていますので、「本記事の内容で分かりにくい個所がある」や「制作のアイデアがあるけれども、壁にぶつかっているのでアドバイスがほしい」などありましたら、遠慮なくご質問ください。
※株を売買する発注系のAPIやリアルタイムで時価情報を取得するPush APIについてまとめた内容の需要がある場合、続編の記事を書こうと思います。


最後までお読みいただき誠にありがとうございました。