下記に書いた1秒足作るコードで作った1秒足データを元に、バックテストを実施するコードを作成してみました。
ただ、Firestoreを使うのをやめようと思ってるので、Firestoreデータをcloud storageにエクスポートして、bigQueryにインポートして、テーブル生成して、cloud storageにエクスポートして、変なJSON形式になっちゃったやつを使っております。わっはっはっは。csvでエクスポート出来なかったんじゃよ。
ルール
ちなみに下記のような運用ルールを想定してます。
- 売買ルール:5秒に1回アクションをとる。(4連続同一サイドで約定すると強制解除。)
- アクション:買う、売る、なにもしない(注文の有効期限は1分)
- 売買価格:直近1秒足の高値と安値の真ん中
- 報酬:約定時の利益変動
- 1秒足データの内容: 始値、終値、安値、高値、買サイズ、売サイズ
ツイッターで流行っているっぽい強化学習をやってみたいので、とりあえずデータを集めて、報酬をあげられるようにしようとしてるんです。
コード
コードです。何も考えずに5秒おきに買って、売ってを繰り返すと、2月13日では、-14280円値幅くらい損したようです。毎回0.2でポジってたら、-2856円位です。テスト不十分なので間違っていたら申し訳ありません。
const fs = require('fs'); const readline = require('readline'); const orderTimeRange = 5; //注文間隔(秒) const orderTimeLimit = 60; //各注文の有効時間(秒) const oneShotSize = 0.2; //1回の注文サイズ const maxPosiSize = 0.6; //保有可能なポジションサイズ const lossCutPriceRate = 0.05; //強制ポジション解除の場合の金額すべらす率(%) let executions = []; //約定データ let orderNum = 0; //全注文回数 let actions = []; //今回のアクションが全てはいった配列 let positions = []; //全ての約定が入った配列 //シミュレーション const main = async () => { await readExecutions(); console.log('executions: ' + executions.length); orderNum = Math.ceil(executions.length / orderTimeRange); console.log('orderNum: ' + orderNum); getActions(); console.log('actions: ' + actions.length); for (let i = 0; i < orderNum; i++) { let idx = orderTimeRange * i; let action = actions[i]; const max = parseInt(executions[idx].maxPrice); const min = parseInt(executions[idx].minPrice); let price = parseInt((max - min) / 2 + min); let executed = checkExecuted(idx, price); if (!executed) continue; positions.push({ action: action, price: price, executed: executed }); } sortPositions(); checkPosi(); }; const checkPosi = () => { const maxPosiCount = Math.ceil(maxPosiSize / oneShotSize); let posiData = []; let profit = 0; let totalProfit = 0; positions.forEach(posi => { if (posiData.length <= 0) { posiData.push(posi); } else { if (posi.action === posiData[0].action) { posiData.push(posi); } else { let old = posiData.shift(); if (old.action === 'BUY') { profit = posi.price - old.price; } else { profit = old.price - posi.price; } } } if (posiData.length > maxPosiCount) { let totalPrice = 0; posiData.forEach(old => { totalPrice += old.price; }); const oldPrice = parseInt(totalPrice / posiData.length); if (posi.action === 'SELL') { profit += posi.price - oldPrice; } else { profit += oldPrice - posi.price; } profit -= posi.price * lossCutPriceRate / 100; posiData = []; } totalProfit += profit; posi.profit = profit; posi.totalProfit = totalProfit; profit = 0; }); // console.log(positions); console.log(totalProfit); console.log(totalProfit * oneShotSize); }; const sortPositions = () => { positions.sort((a, b) => { if (a.executed < b.executed) return -1; if (a.executed > b.executed) return 1; return 0; }); }; //約定できたかどうかを返す // idx + 1の開始時点で注文を出す // idx + 1の終了時点で注文1秒経過 const checkExecuted = (idx, price) => { for (let i = 0; i < orderTimeLimit; i++) { let execIdx = idx + 1 + i; if (execIdx >= executions.length) break; let exec = executions[execIdx]; const max = parseInt(exec.maxPrice); const min = parseInt(exec.minPrice); if (price >= min && price <= max) return execIdx; } return null; }; //今回はとりあえず、買いと売りを繰り返すだけにしてみる。 const getActions = () => { let action = 'BUY'; for (let i = 0; i < orderNum; i++) { actions.push(action); action = action === 'BUY' ? 'SELL' : 'BUY'; } }; //jsonファイルを読み込む const readExecutions = async () => { const fileStream = fs.createReadStream('./one_sec_0213.json'); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); for await (const line of rl) { executions.push(JSON.parse(line)); } }; main();