ビットコイン自動トレードへの道 (番外編) (reduce使ってスライド計算)
これまでjavascriptのconst記述子を使って関数を記述するスタイルのコーディングばっかりだったけど、1ファイルに全部を記述するのは可読性・保守性に難があるので、とりあえず保守性を上げるために
・1関数の長さを短くする。
・関数の集合ではなく、機能・役割ごとに「クラス」にまとめる。
を進めていくことにした。
今回は
1関数の長さを短くする
の一つの手法だけを取り上げることにする。
クラスについては次回以降に譲る。
なにぶんプログラミング的な話なので、自動トレードに直結する話でないのが申し訳のないですが、お付き合い願えればと思っている。
以前にjavascriptのmap, reduce, sliceって便利という話は書いたと思うが、今回はreduceの使い方を一つ。
reduceは以下の構文を持つ。
arr.reduce(callback[, initialValue])
詳しくは以下のサイトを参照して欲しい。
配列に対する操作でmap,reduceを使い始めると常習作用があるのか手放せなくなる。
配列でよく登場する例題の「配列内の数値の合計を求める」は以下のように書くことができる。
// 配列の宣言
var a = [1,2,3,4,5];
// 配列の中の値の合計
var sum = a.reduce((pre,cur,i,arr) => {return pre+cur});
// 結果出力
console.log(sum);
// -> 15
合計値を求めるなどはforで回しても計算は出来るけど「格好悪い」ので、できればスマートにreduceを使ってみたい。(sum関数があればそれに越したことはないが)
上記は単純な数値の配列の例だが、配列がオブジェクトになってももちろんreduceは使える。
例えば、
// オブジェクトの配列
var b = [
{ value: 1 },
{ value: 2 },
{ value: 3 },
{ value: 4 },
{ value: 5 },
];
のようなオブジェクトの配列を考える。
この配列の「値の合計」を求めたいと思った時、mapとreduceを使って
// 上記のオブジェクトの合計値を求める
let ret = b.map(x => x.value).reduce((pre,cur,i,arr) => {return pre+cur});
console.log(ret);
// -> 15
でも期待した結果は得られる。
ただし、ちょっと冗長なところがある。mapでわざわざ数値配列化しなくても良いのだ。
試しに
// 上記のオブジェクトの合計値を求める
let ret = b.reduce((pre,cur,i,arr) => {return pre+cur});
console.log(ret);
と強引に合計計算をしてみよう。
すると結果は
// 結果
// -> [object Object][object Object][object Object][object Object][object Object]
となってしまった。
本当は
// 期待した値
// -> {value: 15}
を期待したにもかかわらず。
では、どのようにすれば良いか? pre,curにはそれぞれオブジェクトが入ってっくるので、returnでもオブジェクトで戻してあげないと次回のpreにオブジェクトが期待したように戻されない。
よって、
// オブジェクト同士を足し合わせるには、その内容をそれぞれ足さないといけないのです。
ret = b.reduce((pre,cur,i,arr) => {return {value: pre.value + cur.value}});
console.log(ret);
// -> { value: 15 }
のようにすれば、一応期待した結果になる。
戻されるのはオブジェクトの形である点がちょっと不都合だが、まだましだろう。オブジェクトがもっと複雑でも欲しい部分だけのオブジェクトにしてしまう方法で良い。
// オブジェクトの配列
var c = [
{ title: 't-1', value: 1 },
{ title: 't-2', value: 2 },
{ title: 't-3', value: 3 },
{ title: 't-4', value: 4 },
{ title: 't-5', value: 5 },
];
の場合は、以下のようにして期待する値を抜き出す。
// 上記のような配列でも、欲しい部分だけを+してあげれば大丈夫
ret = c.reduce((pre,cur,i,arr) => {return {value: pre.value + cur.value}});
console.log(ret);
// -> { value: 15 }
でも本当は「15」という値だけ欲しいことに気がつき、もう一工夫欲しくなる。そもそも、合計値が欲しいのにオブジェクトから再度値を取り出す作業が発生するのはいただけない。
そこで次のようにするともっと簡単に求められることがわかる。
これまでは省略してきた「initialValue」を使うのだ。
すると、preに入ってくる値が配列の先頭ではなく、指定された初期値になる。これは便利。
// initialValueに0を指定する
ret = c.reduce((pre,cur,i,arr) => {return pre + cur.value},0);
console.log(ret);
// -> 15
これでめでたく欲しい値だけを素直に取り出すことに成功した。
上記の例ではあまり恩恵がないが、VWMAなんかを計算するときにはとても重宝する。
例えば VWMAを計算したいと考えると
[ 計算式 ]
終値0×出来高0+終値1×出来高1+・・・終値n×出来高n
VWMA = -------------------------------------------------------
出来高0+出来高1+・・・出来高n
なので、以下のようにすると期待する値を得ることができる。
入力(rows)には以下のようなOHLCVの配列を渡す。
[{ timestamp: 値, open: 値, high: 値, low: 値, close: 値, volume: 値 }
{ timestamp: 値, open: 値, high: 値, low: 値, close: 値, volume: 値 }
{ timestamp: 値, open: 値, high: 値, low: 値, close: 値, volume: 値 }
.....
]
期間ごとに配列をsliceで取り出して、reduceしてあげるだけだ。
// 計算期間
const period = 14;
// 結果配列
var vwma = [];
for(let i=0;i<(rows.length - period + 1);i++) {
const t = rows[i + period - 1].timestamp;
const u = rows.slice(i, i + period).reduce((pre, cur, i, arr) => { return pre + cur.close * cur.volume; }, 0);
const d = rows.slice(i, i + period).reduce((pre, cur, i, arr) => { return pre + cur.volume; }, 0);
vwma.push({timestamp: t, vwma: u/d});
}
上記で計算した結果は
[ { timestamp: 1526328000, vwma: 8634.967742904597 },
{ timestamp: 1526331600, vwma: 8655.185204684089 },
{ timestamp: 1526335200, vwma: 8665.05470385281 },
{ timestamp: 1526338800, vwma: 8680.981257010952 },
{ timestamp: 1526342400, vwma: 8700.506804110551 },
{ timestamp: 1526346000, vwma: 8715.480997149312 },
{ timestamp: 1526349600, vwma: 8727.645473221386 },
...(中略)...
]
のように出力される。
とても便利ですね🎵
皆様もjavascriptの配列操作で「もうちょっとなんとかならない?」と悩んだ時にはslice,map,reduceを便利に使ってくださいね〜
ソフトウェア・エンジニアを40年以上やってます。 「Botを作りたいけど敷居が高い」と思われている方にも「わかる」「できる」を感じてもらえるように頑張ります。 よろしくお願い致します。