CandleCast は、株価などの OHLC 時系列をローソク足で描き、移動平均・ボリンジャーバンド・RSI・MACD を重ねるチャートツールです。テクニカル指標は全部自前で計算しています。「途中まで値が出ない」ことをどう扱うかが、実装のほとんどでした。
#🎯 指標は「最初のほうが計算できない」
移動平均を 25 日で取ると、最初の 24 日ぶんは値が出せません。RSI は 14 日、MACD は 26 日 + 9 日。指標ごとに助走期間が違います。
ここを雑に扱うと、
- 0 で埋めて、チャートの左端に存在しない急落が描かれる
- 配列の長さがずれて、日付と値が 1 本ずつずれる
という事故になります。実際、株価チャートで見かける不自然な線の多くはこれです。
なので全指標の戻り値を Array<number | null> に統一しました。値がない期間は null で、描画側は null を飛ばして線を切ります。長さは常に元データと同じなので、添字がそのまま日付に対応します。
export function sma(values: number[], period: number): Array<number | null>
export function rsi(values: number[], period = 14): Array<number | null>型で「出ない期間がある」ことを表明しておくと、使う側が null を潰さざるを得なくなります。0 埋めより面倒ですが、面倒なぶん間違えません。
#📈 RSI ── Wilder の平滑化
RSI は「上がった幅と下がった幅の比率」です。素朴に単純移動平均で書きたくなりますが、本来の定義は Wilder の平滑化という別物です。
if (i <= period) {
avgGain += gain
avgLoss += loss
if (i === period) {
avgGain /= period // 最初の 1 回だけ 単純平均
avgLoss /= period
}
} else {
// 以降は 前回の平均を (period-1) 倍して 新しい値を足す
avgGain = (avgGain * (period - 1) + gain) / period
avgLoss = (avgLoss * (period - 1) + loss) / period
}最初の 1 回だけ単純平均、以降は指数的に減衰させるという二段構えです。単純移動平均で計算すると、証券会社のチャートと数値が合いません。指標は「定義が 1 つに決まっている」ので、近い値ではなく同じ値が出る必要があります。
avgLoss === 0 のときに rs が無限大になるので、そこは 100 に倒しています。連騰が続くと実際に起きます。
#🔀 MACD ── null を挟んだ EMA をどう繋ぐか
MACD は 12 日 EMA と 26 日 EMA の差、そのシグナルは MACD 線自体の 9 日 EMA です。
ここで問題になるのが、MACD 線の先頭に null が並んでいること。そのまま EMA にかけると null が数値として混ざります。
そこで有効な値だけを抜き出して EMA を取り、元の位置に戻すという手順にしました。
const validLine: number[] = []
const lineIdxMap: number[] = []
for (let i = 0; i < line.length; i++) {
if (line[i] !== null) {
validLine.push(line[i] as number)
lineIdxMap.push(i) // 元の添字を覚えておく
}
}
const signalValid = ema(validLine, 9)
// 覚えた添字を使って 元の配列へ書き戻す
for (let j = 0; j < signalValid.length; j++) {
if (signalValid[j] !== null) signal[lineIdxMap[j]] = signalValid[j]
}添字の対応表を持っておくのが素直な解でした。詰めて計算して、戻すときに写す。この形にすると、助走期間が二重(26 日 + 9 日)になっても位置がずれません。
#📊 サンプルデータを GBM で作る
株価 API を叩くと、無料枠・レート制限・利用規約の問題が出ます。かといって空のチャートを見せても何も分かりません。
そこで幾何ブラウン運動(GBM)で仮想の値動きを生成しました。日経・TOPIX・S&P500・自動車・テックの 5 銘柄ぶんです。
GBM は「価格の変化率が正規分布に従う」というモデルで、実際の株価の性質(価格が負にならない、変動幅が価格に比例する)を持ちます。架空の値だと明示したうえで、指標の挙動を確かめるには十分です。
自前の OHLCV CSV も投げ込めるようにしてあるので、本物のデータを見たい人はそちらを使えます。
#🕯️ ローソク足の描画
1 本のローソクは 4 つの値(始値・高値・安値・終値)を持ちます。描くものは 2 つ。
- ヒゲ = 高値から安値までの縦線
- 実体 = 始値と終値の間の矩形。 終値 > 始値なら陽線、逆なら陰線
日本式では陽線が赤(または白)、陰線が青(または黒)です。海外式は逆で緑と赤。ここは日本式にしました。
SVG を三段(価格 / RSI / MACD)に分けて、横軸のスケールだけ共有しています。縦軸は指標ごとに値域が違う(価格は数万、RSI は 0〜100、MACD は 0 をまたぐ)ので、それぞれ独立に取ります。
#😓 苦労したところ
CSV の列名がバラバラ。 Date/Open/High/Low/Close/Volume の順とは限らず、日本語ヘッダのこともあります。ヘッダ名を正規化して照合し、見つからなければ位置で推測する二段構えにしました。
指標を全部出すと読めない。 4 つ全部を初期表示にすると線だらけになります。既定は移動平均だけにして、他はトグルで足す形にしました。
投資助言にしない。 テクニカル指標は「買い」「売り」のシグナルとして語られますが、そう書くと投資助言になります。指標の定義と計算方法は説明し、売買の判断は書かないという線を引きました。数字を出すサービスで一番気を使う部分です。
#🔭 今後の拡張
- 一目均衡表、出来高加重平均(VWAP)
- 期間の任意指定(現在は 1ヶ月 / 3ヶ月 / 半年 / 1年)
- 複数銘柄の重ね表示
- チャート画像の書き出し
#💡 このサービスから言えること
null を型に出すと、時系列の計算はほぼ壊れなくなります。 0 埋めや配列の切り詰めで済ませると、必ずどこかで日付とのずれが出ます。「長さは元データと同じ、出ない期間は null」を全指標で守っただけで、描画側の分岐が消えました。
そして定義が決まっているものは、近い値ではなく同じ値を出すこと。RSI を単純移動平均で書けば「それらしい線」は引けますが、他のチャートと数字が合わなければ道具として使えません。
[ ./next_action ]
読んだら、 CandleCast を実際に動かす。
この開発ログは CandleCast をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。