AI Dev Lab
CandleCast

CandleCast ができるまで — ローソク足と 4 指標を自前計算

株価チャートツールの開発ログ。 OHLC の CSV を読み、 移動平均・ボリンジャーバンド・RSI・MACD を 素の JavaScript で自前計算して、 価格と指標を三段の SVG に描くまでを記録しています。

CandleCast は、株価などの OHLC 時系列をローソク足で描き、移動平均・ボリンジャーバンド・RSI・MACD を重ねるチャートツールです。テクニカル指標は全部自前で計算しています。「途中まで値が出ない」ことをどう扱うかが、実装のほとんどでした。

#🎯 指標は「最初のほうが計算できない」

移動平均を 25 日で取ると、最初の 24 日ぶんは値が出せません。RSI は 14 日、MACD は 26 日 + 9 日。指標ごとに助走期間が違います。

ここを雑に扱うと、

  • 0 で埋めて、チャートの左端に存在しない急落が描かれる
  • 配列の長さがずれて、日付と値が 1 本ずつずれる

という事故になります。実際、株価チャートで見かける不自然な線の多くはこれです。

なので全指標の戻り値を Array<number | null> に統一しました。値がない期間は null で、描画側は null を飛ばして線を切ります。長さは常に元データと同じなので、添字がそのまま日付に対応します。

ts
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 の平滑化という別物です。

ts
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 を取り、元の位置に戻すという手順にしました。

ts
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 をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ToonCast

ToonCast ができるまで — AnimeGANv2 をブラウザで動かす

AnimeGANv2 の小さな ONNX (約9MB) を onnxruntime-web (単一スレッド WASM=COOP/COEP不要、 color-revive で承認済みライブラリの再利用) で実行。 512x512・[-1,1] 正規化で推論し、 結果を元解像度に戻して表示する設計記録。 写真は端末内処理。

read log →
ColorRevive

ColorRevive ができるまで — onnxruntime-web で白黒写真をカラー化

DeOldify の量子化 ONNX を onnxruntime-web (CDN side-load・単一スレッド WASM=COOP/COEP不要) で実行。 256x256 でモデル推論し、 輝度は元写真・色だけ AI を YCbCr で再合成して輪郭を保つ設計記録。 写真は端末内処理。

read log →
PhotoTwin

PhotoTwin ができるまで — CLIP画像埋め込みで似た写真を見つける

CLIP (Xenova/clip-vit-base-patch32) の image-feature-extraction を transformers.js の CDN ESM で side-load し、 各写真を正規化ベクトル化。 cosine 類似度で重複・似た写真をブラウザ内で検出する設計記録 (新ライブラリ追加なし=what-cam と同じ CLIP の再利用)。

read log →
AkinFind

AkinFind ができるまで — ブラウザ内で完結する embeddings 意味検索

多言語の文章埋め込みモデル (Xenova/multilingual-e5-small) を transformers.js の CDN ESM で side-load し、 各文を正規化ベクトル化。 cosine 類似度で意味検索と似ている文ペア検出を全て端末内で行う設計記録。

read log →
WhatCam

WhatCam ができるまで — CLIP のゼロショット画像分類をブラウザで動かす

CLIP (Xenova/clip-vit-base-patch32) を transformers.js の CDN ESM で side-load し、 写真と候補ラベルの近さをブラウザ内で計算。 日本語ラベルを英語プロンプトに変換し、 図鑑と自由入力の両モードで「これ何?」を判定する設計記録。

read log →
DepthCast

DepthCast ができるまで — 1枚の写真をAIの深度推定で立体にする

Depth Anything (transformers.js) を CDN ESM で side-load し、 1枚の写真から深度マップを推定。 WebGL2 フラグメントシェーダで深度に比例した視差 (iterative backward parallax) を作り、 赤青アナグリフ / WebM 書き出しまで端末内で完結させた設計記録。

read log →