指数化(相対)チャートを一度に4チャート以上見たいことがあったのでローソク足・出来高チャートに追加してみた。無理やり追加したので、ソースコードはブサイクになってしまったが、別に製品ではないので、要件を満たせばよい。
証券会社で提供されているトレーディングツールでは、指数化(相対)チャートを表示できるが、多くても4(銘柄)チャートまでのようである。少なくても自分が使っているSBI証券のHiper SBI、楽天証券のマーケットスピードにおいては、3~4チャートまでである。(Hiper SBIでは相対チャート、マーケットスピードでは指数化チャート)ツール比較サイトで調べても最大5銘柄までのようである。
| 証券会社 | ツール名 | チャート数 |
| SBI 証券 | HYPER SBI | 相対チャートで3つまで比較できる |
| 楽天証券 | MARKET SPEEDⅡ | 指数化チャートで4つまで比較できる |
マーケットスピードでは指数化チャートとよんでいて「基準日となる株価を100としてそこからの暴落率を指数としてあらわしたもの」のようである。
HYPER SBIでは相対チャートとよんでいて、「同期間のなかで、始点から各銘柄の値上がり率がどのように推移したかを表示させる」とある。
Yahoo!ファイナンスでは、比較チャートでみることができるが、こちらは基準日を0として表示しているようだ。
指数化チャートも相対チャートも下記の計算式でよさそうである。
(ある日付の終値/基準日の終値)× 100
よさそうであるというのは、明確に説明書に書いてないのだが、チャート表示させると2つは明らかに同じで、ある期間の最終相対値が一致する。(仕様は変更される可能性もあるので、もちろん保証はできない)
ウクライナ侵攻があってからの海外ETFや商品ETFの動きに興味があって、相対チャートを4チャート以上一度に強弱が見たいというのが動機であった。他にも銘柄発掘サイトで過去に特集した銘柄群のパフォーマンスはどうなのか追っかけたい。というのもあった。
指数化(相対)チャートについては、C#に関数がみつからなかったので、自作することにしました。
操作方法
操作について、TreeViewルートのカテゴリを選択して「検索」ボタンをクリックすると描画できるようにします。
日経平均が休場日の時はチャート表示していない。日経平均が稼働日で、個別銘柄が休場日の時は、休場日として、データ抜けした状態を表示しています。(チャート上では非実数をいれている)

コントロールへの追加
指数化チャート表示のため、ChartAreaへChartArea3を追加します。
| コントロール | コレクション | 追加するメンバー |
| chart1 | ChartArea | ChartArea3 |
PanActiveDBAc.csの変更
Pricesクラスへの追加
新たに、2つのCreateSerialDataを追加します。
1.1つは、銘柄コードに対する日付リストを返すメソッドです。
銘柄コードの日付リストを要素がDateTimeのListで返します。
稼働日の時だけAddメソッドを使って日付をListに追加しています。
if(prices.IsClosed(i) == 0)
listdate.Add(calendar.Date(i).Date);稼働日のカレンダーを作成します。
// 2022/4/17 Start
/// <summary>
/// 日付リストを返す
/// </summary>
/// <param name="code">銘柄コード</param>
/// <returns>SortedDictionary 日付リスト</returns>
public List<DateTime> CreateSerialData(string code)
{
var listdate = new List<DateTime>();
Calendar calendar = new Calendar();
// 銘柄の最初の日付位置
int begin = 0;
// 銘柄の最終の日付位置
int end = 0;
try
{
// 四本値と出来高を読み込む
Read(code);
// 日付位置の最小値と最大値を読み込む
end = End();
begin = Begin();
}
catch (Exception a)
{
// 例外処理
//イミディエイトウィンドウにエラー表示
Console.WriteLine(a.Message);
listdate = null;
return listdate;
}
for (int i = begin; i <= end; i++)
{
if (IsClosed(i) == false)
// 稼働日
listdate.Add(calendar.Date(i).Date);
}
return listdate;
}2.2つ目は、日付をキーとして値をStockオブジェクトとするコレクションを返します。コレクションはSortedDictionaryクラスで、前回作成したGetDataTableの改良版です。
| Key | Value |
| DateTime | Stockオブジェクト |
DateTime型がキーでもソートされるのか試してみました。以下サンプルコードです。途中、objStockを作成して、Valueとして格納していますが、実際は未使用です。
SortedDictionary<DateTime, object> objSdata = new SortedDictionary<DateTime, object>();
prices.Read("1001");
int position = calendar.DatePosition(DateTime.Parse("2022/4/15"));
Console.WriteLine("---要素の格納---");
for (int i = position; i > (position - 3); i--)
{
if (prices.IsClosed(i) == false)
{
// 稼働日
Stock objStock = new Stock();
try
{
objStock.date = calendar.Date(i);
objStock.high = prices.High(i);
objStock.low = prices.Low(i);
objStock.open = prices.Open(i);
objStock.close = prices.Close(i);
objStock.volume = prices.Volume(i);
}
catch (Exception a)
{
//イミディエイトウィンドウにエラー表示
Console.WriteLine(a.Message);
continue;
}
Console.WriteLine(objStock.date);
objSdata.Add(objStock.date, objStock);
}
}
Console.WriteLine("---要素の取出し---");
foreach ( var kvPair in objSdata )
{
Console.WriteLine(kvPair.Key);
}実行結果(イミディエイトウィンドウ)
—要素の格納—
2022/04/15 0:00:00
2022/04/14 0:00:00
2022/04/13 0:00:00
—要素の取出し—
2022/04/13 0:00:00
2022/04/14 0:00:00
2022/04/15 0:00:00
ちゃんと昇順に取り出せていますね。
個別銘柄が休場日の場合(IsClosedの戻り値)データが欠落しているのかわからないので、日経平均が休場日かどうか調べています。
| 個別銘柄 | 日経平均 | objSdata |
| 稼働日 | ー | 要素を追加 |
| 休場日 | 稼働日 | データ欠落として処理 |
| 休場日 | 休場日 | 休場日とする 要素を追加しない |
/// <summary>
/// Panデータから4本値を返す
/// </summary>
/// <param name="code">銘柄コード</param>
/// <param name="beginDay">最初の日付</param>
/// <param name="endDay">最終の日付</param>
/// <param name="sd1001">日経平均の日付リスト</param>
/// <param name="nan">休場日の時に非数を代入するかどうか</param>
/// <returns>SortedDictionary 日付をキーとした4本値を返す</returns>
public SortedDictionary<DateTime, object> CreateSerialData(string code, DateTime beginDay, DateTime endDay,
List<DateTime> sd1001, bool nan = false)
{
SortedDictionary<DateTime, object> objSdata =
new SortedDictionary<DateTime, object>();
Calendar calendar = new Calendar();
// 銘柄の最初の日付位置
int begin = 0;
// 銘柄の最終の日付位置
int end = 0;
try
{
// 四本値と出来高を読み込む
Read(code);
// 日付位置の最小値と最大値を読み込む
end = End();
begin = Begin();
}
catch (Exception a)
{
// 例外処理
//イミディエイトウィンドウにエラー表示
Console.WriteLine(a.Message);
objSdata = null;
return objSdata;
}
// 最初の日付位置を取得
int dataPosBegin = calendar.DatePosition(beginDay, -1);
// 最終の日付位置を取得
int dataPosEnd = calendar.DatePosition(endDay, -1);
// 最初の日付と最終日付を補正
if (dataPosBegin <= end && dataPosEnd >= begin)
{
if (dataPosBegin >= begin)
{
begin = dataPosBegin;
}
if (dataPosEnd < end)
{
end = dataPosEnd;
}
}
else
{
// 指定した期間にデータが存在しない
objSdata = null;
return objSdata;
}
for (int i = begin; i <= end; i++)
{
if (IsClosed(i) == false)
{
// 稼働日
Stock objStock = new Stock();
try
{
objStock.date = calendar.Date(i);
objStock.high = High(i);
objStock.low = Low(i);
objStock.open = Open(i);
objStock.close = Close(i);
objStock.volume = Volume(i);
}
catch (Exception a)
{
//イミディエイトウィンドウにエラー表示
Console.WriteLine(a.Message);
continue;
}
objSdata.Add(objStock.date, objStock);
}
else
{
{
// 日経平均が休場日の時は、ほんとに休場日
if (nan == true)
{
if (sd1001.Contains(calendar.Date(i).Date))
{
// 日経平均は、稼働日
Stock objStock = new Stock();
// 休場日には、非数を代入する。
objStock.date = calendar.Date(i).Date;
objStock.high = Double.NaN;
objStock.low = Double.NaN;
objStock.open = Double.NaN;
objStock.close = Double.NaN;
objStock.volume = Double.NaN;
objSdata.Add(objStock.date, objStock);
}
}
}
// 日経平均が休場日の時は、ほんとに休場日
}
}
return objSdata;
}
// 2022/4/17 Endクラスの追加
日付、株価、出来高を保持するクラスを追加します。
// 2022/4/17 Start
class Stock
{
private DateTime _date;
private double _open;
private double _close;
private double _high;
private double _low;
private double _volume;
/// <summary>
/// 日付の設定と取得
/// </summary>
public DateTime date
{
get { return _date; }
set { _date = value; }
}
/// <summary>
/// 始値の設定と取得
/// </summary>
public double open
{
get { return _open; }
set { _open = value; }
}
/// <summary>
/// 終値の設定と取得
/// </summary>
public double close
{
get { return _close; }
set { _close = value; }
}
/// <summary>
/// 高値の設定と取得
/// </summary>
public double high
{
get { return _high; }
set { _high = value; }
}
/// <summary>
/// 安値の設定と取得
/// </summary>
public double low
{
get { return _low; }
set { _low = value; }
}
/// <summary>
/// 出来高の設定と取得
/// </summary>
public double volume
{
get { return _volume; }
set { _volume = value; }
}
}
// 2022/4/17 EndForm1.csの変更
Form1クラスへメソッドの追加と変更を行います。
Form1_Loadへの追加
表示期間「3か月」を追加します。追加するコードはStart ~ End のコメントで挟んでいる箇所。
row = dataPeriod.NewRow();
row["表示期間"] = "2か月";
row["データ列"] = 2;
dataPeriod.Rows.Add(row);
// 2022/4/17 Start
row = dataPeriod.NewRow();
row["表示期間"] = "3か月";
row["データ列"] = 3;
dataPeriod.Rows.Add(row);
// 2022/4/17 End
row = dataPeriod.NewRow();
row["表示期間"] = "6か月";
row["データ列"] = 6;
dataPeriod.Rows.Add(row);relativeViewメソッドの追加
指数化(相対)チャートを表示させる本体です。追加したChartArea3へ表示するように紐づけしてあります。基準となる日がデータ欠落している場合、NaNデータが設定されるようになっています。NaNであってもそのまま計算してchart1.Seriesにつっこんでも問題なさそうである(なんと乱暴な)。NaN以外のデータがはじめてみつかって値を基準値としている。
戻り値として、日付をキーとしてStockオブジェクトを値としたSortedDictionary型を返します。VB6.0を使用してたころは、なんでDictionary型はソートされないんだ使いにくいと思っていましたが、便利なクラスが追加されているようです。
凡例には、最終日付の相対値をくっつけています。
// 2022/4/17 Start
/// <summary>
/// 相対チャートを表示する
/// </summary>
/// <param name="code">銘柄コード</param>
/// <returns>相対チャートのシリアルデータ</returns>
private SortedDictionary<DateTime, object> relativeView(string strCode)
{
List<DateTime> lstSdata;
lstSdata = prices.CreateSerialData("1001"); // 日経平均のシリアルデータ
SortedDictionary<DateTime, object> objSdata; // key = DateTime, Value = Stock
int value = Convert.ToInt32(cmbPeriod.SelectedValue);
if (value < 0)
{
// 期間指定
objSdata = prices.CreateSerialData(strCode, dateTPbegin.Value, dateTPend.Value, lstSdata, true);
}
else
{
// 月数指定
DateTime endDate = dateTPend.Value;
DateTime beginDate = endDate.AddMonths(0 - value);
objSdata = prices.CreateSerialData(strCode, beginDate, endDate, lstSdata, true);
}
if (allCodeNames.dict.ContainsKey(strCode) == false)
{
// 存在しないコード
return objSdata;
}
if (objSdata is null)
{
// オブジェクトが存在しない。
return objSdata;
}
int dataCount = objSdata.Count;
if (dataCount == 0)
{
// データなし
objSdata = null;
return objSdata;
}
string strName = allCodeNames.dict[strCode];
string lineLegend = strName;
chart1.Series.Add(lineLegend);
// 時系列データとチャートの紐づけ
chart1.Series[lineLegend].ChartArea = "ChartArea3";
chart1.Series[lineLegend].ChartType = SeriesChartType.Line;
chart1.Series[lineLegend].XValueType = ChartValueType.Date;
chart1.Series[lineLegend].IsXValueIndexed = true;
chart1.Series[lineLegend].ToolTip = lineLegend + " 日付:#VALX\n相対値:#VALY{N2}";
double dblRelative = 100;
double dblTop = Double.NaN;
// 相対チャートの表示
foreach (var kvPair in objSdata)
{
Stock objStock = (Stock)kvPair.Value;
if (Double.IsNaN(dblTop))
{
if (Double.IsNaN(objStock.close) == false)
{
// 最初の非数でない値を初期値とする
dblTop = objStock.close;
}
}
else
{
dblRelative = (objStock.close / dblTop) * 100;
}
dblRelative = Math.Round(dblRelative, 2, MidpointRounding.AwayFromZero);
chart1.Series[lineLegend].Points.AddXY(kvPair.Key, dblRelative);
}
// 最終日付の相対値
strName = strName + " " + dblRelative.ToString();
chart1.Series[lineLegend].LegendText = strName;
return objSdata;
}btnView_Clickイベントの変更
指数化チャートを表示するため、全面改訂しました。
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnView_Click(object sender, EventArgs e)
{
prices.AdjustExRights = true; // 権利落ち修正した価格を読み込む。
TreeNode treeNode = treeView1.SelectedNode;
string strCode;
if (treeNode != null)
{
if (treeNode.Parent == null)
{
TreeNode ParentNode = treeView1.SelectedNode;
if (ParentNode != null)
{
if (ParentNode.Parent == null)
{
int x = ParentNode.Nodes.Count;
Console.WriteLine(x.ToString());
chart1.Titles.Clear();
chart1.ChartAreas["ChartArea1"].Visible = false;
chart1.ChartAreas["ChartArea2"].Visible = false;
chart1.ChartAreas["ChartArea3"].Visible = true;
// マージンあり
chart1.ChartAreas["ChartArea3"].AxisX.IsMarginVisible = true;
// 軸の最小値を0に設定しない
chart1.ChartAreas["ChartArea3"].AxisY.IsStartedFromZero = false;
TreeNode ChildNode = ParentNode.FirstNode;
lstCode.Clear();
lstCode.Add(ChildNode.Text);
chart1.Series.Clear();
objCodeStock.Clear();
objCodeStock.Add(ChildNode.Text, relativeView(ChildNode.Name));
while ((ChildNode = ChildNode.NextNode) != null)
{
lstCode.Add(ChildNode.Text);
objCodeStock.Add(ChildNode.Text, relativeView(ChildNode.Name));
}
}
}
return;
}
else
{
strCode = treeNode.Name;
}
}
else
{
// TreeViewに読み込まれていない
strCode = txtCode.Text;
}
chart1.ChartAreas["ChartArea1"].Visible = true;
chart1.ChartAreas["ChartArea2"].Visible = true;
chart1.ChartAreas["ChartArea3"].Visible = false;
string legend = strCode;
int value = Convert.ToInt32(cmbPeriod.SelectedValue);
List<DateTime> sd1001;
// 日経平均のシリアルデータ
sd1001 = prices.CreateSerialData("1001");
SortedDictionary<DateTime, object> sd;
if (value < 0)
{
// 期間指定
sd = prices.CreateSerialData(legend, dateTPbegin.Value, dateTPend.Value, sd1001);
}
else
{
// 月数指定
DateTime endDate = dateTPend.Value;
DateTime beginDate = endDate.AddMonths(0 - value);
sd = prices.CreateSerialData(legend, dateTPbegin.Value, dateTPend.Value, sd1001);
}
if (allCodeNames.dict.ContainsKey(legend) == false)
{
// 存在しないコード
return;
}
if (sd is null)
{
// オブジェクトが存在しない。
return;
}
int dataCount = sd.Count;
if (dataCount == 0)
{
// データなし
return;
}
// 銘柄名を取得する
string strName = allCodeNames.dict[strCode];
chart1.Titles.Clear();
chart1.Titles.Add(strName);
// ローソク足と移動平均の設定
chart1.ChartAreas["ChartArea1"].AxisX.ScaleView.Size = dataCount;
// マージンあり
chart1.ChartAreas["ChartArea1"].AxisX.IsMarginVisible = true;
// 軸の最小値を0に設定しない
chart1.ChartAreas["ChartArea1"].AxisY.IsStartedFromZero = false;
// 2022/02/11 start
// 出来高チャートエリアの設定
chart1.ChartAreas["ChartArea1"].AlignWithChartArea = "ChartArea2";
// マージンあり
chart1.ChartAreas["ChartArea2"].AxisX.IsMarginVisible = true;
// 軸の最小値を0に設定しない
chart1.ChartAreas["ChartArea2"].AxisY.IsStartedFromZero = false;
chart1.ChartAreas["ChartArea2"].AxisX.ScaleView.Size = dataCount;
// 2022/02/11 end
//グラフ初期化(系列を全て削除)
chart1.Series.Clear();
//ローソク足データを追加
chart1.Series.Add(legend);
chart1.Series[legend].ChartType = SeriesChartType.Candlestick;
// 時系列データとチャートの紐づけ
chart1.Series[legend].ChartArea = "ChartArea1";
//凡例に表示するテキストを指定
chart1.Series[legend].LegendText = "株価";
chart1.Series[legend].IsXValueIndexed = true;
chart1.Series[legend]["PriceUpColor"] = Color.Red.Name;
chart1.Series[legend]["PriceDownColor"] = Color.Blue.Name;
chart1.Series[legend].ToolTip = "日付:#VALX\n高値:#VALY\n安値:#VALY2\n始値" +
":#VALY3\n終値:#VALY4";
// ローソク足の表示
int i = 0;
double high;
double low;
double open;
double end;
// 2022/3/16 start
double prebegin = 0; // 前日の始値
double preend = 0; // 前日の終値
// 2022/3/16 end
foreach (var kvPair in sd)
{
Stock st = (Stock)kvPair.Value;
high = st.high;
low = st.low;
open = st.open;
end = st.close;
// 2022/3/16 start 平均足に変換する
if (radioButton2.Checked == true)
{
if (i == 0)
{
end = (st.open + low + high + end) / 4;
open = (end + open) / 2; // 平均足の始値
open = Math.Round(open, MidpointRounding.AwayFromZero); // 四捨五入
}
else
{
end = (open + low + high + end) / 4;
open = (preend + prebegin) / 2; // 平均足の始値
open = Math.Round(open, MidpointRounding.AwayFromZero); // 四捨五入
}
}
preend = end;
prebegin = open;
// 2022/3/16 end
chart1.Series[legend].Points.AddXY(kvPair.Key, st.high);
chart1.Series[legend].Points[i].YValues[1] = low;
chart1.Series[legend].Points[i].YValues[2] = open;
chart1.Series[legend].Points[i].YValues[3] = end;
value = chart1.Series[legend].Points.Count;
i++;
}
chart1.DataManipulator.FinancialFormula(FinancialFormula.MovingAverage, numericMA.Value.ToString(), legend + ":Y4", "SMA");
// 時系列データとチャートの紐づけ
chart1.Series["SMA"].ChartArea = "ChartArea1";
chart1.Series["SMA"].ChartType = SeriesChartType.Line;
chart1.Series["SMA"].ChartArea = "ChartArea1";
chart1.Series["SMA"].Legend = chart1.Series[legend].Legend;
chart1.Series["SMA"].IsXValueIndexed = true;
chart1.Series["SMA"].LegendText = "単純移動平均";
chart1.Series["SMA"].ToolTip = "日付:#VALX\n移動平均値:#VALY";
// 系列間のデータ数を合わせるためにデータを補完する
for (i = dataCount - chart1.Series["SMA"].Points.Count - 1; i >= 0; i--)
{
DataPoint newDataPoint = new DataPoint();
newDataPoint.XValue = chart1.Series[legend].Points[i].XValue;
// 空のデータを設定する
newDataPoint.IsEmpty = true;
chart1.Series["SMA"].Points.Insert(0, newDataPoint);
}
// 2022/02/11
string volumeLegend = "volume";
//出来高系列の追加
chart1.Series.Add(volumeLegend);
// 時系列データとチャートの紐づけ
chart1.Series[volumeLegend].ChartArea = "ChartArea2";
chart1.Series[volumeLegend].ChartType = SeriesChartType.Column;
chart1.Series[volumeLegend].LegendText = "出来高";
chart1.Series[volumeLegend].XValueType = ChartValueType.Date;
chart1.Series[volumeLegend].IsXValueIndexed = true;
// 出来高の表示
foreach (var kvPair in sd)
{
Stock st = (Stock)kvPair.Value;
chart1.Series[volumeLegend].Points.AddXY(kvPair.Key, st.volume);
}
}
// 2022/4/17 End実行結果
ウクライナ侵攻のあったとされる2022年2月24日~4月19日の指数化チャートを表示してみます。
ぱっとしない日経平均に対して、商品(原油や金)が上げているのがよくわかる。海外先進国ETFも上げているが、どうやら為替が影響しているみたいだ。楽天証券に記事があった。それにしても、日経平均も侵攻のあった日からそれほど下げていない。ロシアウクライナ侵攻で戦術核が使われるかもしれんというのに。。。
同じ期間をマーケットスピードで表示してみる(こちらは4つまでだけど)

指数だけを拡大してみると、自作チャートと一致している。

次回
次回には、期間内に2銘柄を乗り換えて売買をしたらどうなるかシュミレションみたいなことをやっていきたいと思うが、Panのチャートギャラリー4でデータ取得するとデータが結構欠落しているので、正確なテストは望めないと思うので知的好奇心を満たす程度にとどめておきます。(商品やETFは大丈夫そう)、近い将来チャートギャラリー5にバージョンアップすることで対処する方針です。

