edo1z blog

プログラミングなどに関するブログです

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

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

blog.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();