BIZ

Bitflyer FX バックテストしてみる

下記に書いた1秒足作るコードで作った1秒足データを元に、バックテストを実施するコードを作成してみました。

https://blog.logicky.com/2019/02/19/130014blog.logicky.com

ただ、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();