Tana Gone
Tana Gone
5 min read

Categories

模擬売買とは売買シミュレーション、システムトレードと言われてるヤツである。コードで記述可能なルールで銘柄を選び、買い取引、売り取引のタイミングも選び、ルールを過去の株価に適用する。適用した結果、利益率(収益: 利益‐損失を取引回数で割る)が大きいルールの発見を目指す。ルール、コード、実行結果を以下に示す。

  • ルール
    1. A銘柄、B銘柄、C銘柄のペアを作りC銘柄を取引対象とする
    2. A - Cペアの値上がり率に相関があり、かつA - Bペアの値上がり率に相関があれば取引対象とする
    3. 値上がり率相関係数は過去100回の立会日の内、任意の連続する6日を選び(day1 ~ day6)、A銘柄のday1終値からday2終値の値上がり率とC銘柄のday2終値からday3終値の値上がり率(遅延値上がり)を100日分算出の上、その相関係数を算出する。更にC銘柄のday3終値からday4終値の値上がり率を100日分算出の上、その相関係数を算出する。これをC銘柄のday6終値からday7終値の値上がり率を100日分算出の上、その相関係数を算出するまで行う。 5つの相関係数が得られるがその最大値も同時に算出する。
    4. 相関係数5個の最大値が基準(0.3)を超えるとA - CペアはAが上がればCが遅れて上がるとみなす。
    5. A - Bペアに関しても同じ様にCが遅れて値上がるペアを探す。
    6. A, B, Cのペアに関して次の売買ルールを適用してC銘柄をトレードする。
    7. 過去100回の立会日から3連続(day1、day2、day3とする)を取り出し、A銘柄day2終値>day1終値かつB銘柄day2終値>day1終値であればC銘柄day3の寄り付きで買いでエントリーしday3の終値でイグジットする。
  • ルール設定の意図

銘柄どおしが関連して値動きする事は市場を観測しているとよく見られる現象である。電線株、海運株が似たような動きをするのを過去の株価を見る事ができる。先行して動く株と遅れて動く株を見つけようと試みているのが今回のルールである。

  • コード

    試運転用に1301から30銘柄だけを取り出しその中からA, B, C銘柄のペアを作る。結果は勝率順に10個のペアをstdoutに出力する。ペアの数は30x29x28/2 = 12,180通り。

    #!/usr/bin/env ruby
    require 'sqlite3'
    require 'date'
    $stdout.sync = true
      
    class StockAnalyzer
      def initialize
        @yatoday_db_path = "/path/to/yatoday.db" # codeTbl
        @crawling_db_path = "/path/to/crawling.db" # Historical data e.g. '1301', '130A'
        @yatoday_db = SQLite3::Database.new(@yatoday_db_path)
        @crawling_db = SQLite3::Database.new(@crawling_db_path)
          
        # 結果をハッシュとして返すよう設定
        @yatoday_db.results_as_hash = true
        @crawling_db.results_as_hash = true
      end
      
      def get_stock_codes
        # 銘柄一覧を取得
        puts "銘柄情報を取得中..."
        tmp_codes = @yatoday_db.execute("SELECT * FROM codeTbl ORDER BY CODE ASC LIMIT 100;")
        puts tmp_codes.first[:code]
        puts "取得した銘柄数: #{tmp_codes.length}"
        codes = tmp_codes.reject {|e| e["code"] < "1000"} 
      end
      
      def get_stock_data(code, days = 150)
        # 指定した銘柄の過去データを取得(日付降順で最新150日分)
        begin
          query = "SELECT date, open, close FROM '#{code}' ORDER BY date DESC LIMIT #{days}"
          data = @crawling_db.execute(query)
            
          # 日付昇順に並び替え
          data.reverse.map do |row|
            {
              date: Date.parse(row['date']),
              open: row['open'].to_f,
              close: row['close'].to_f
            }
          end
        rescue SQLite3::SQLException => e
          puts "データ取得エラー (#{code}): #{e.message}"
          []
        end
      end
      
      def calculate_correlation(data_a, data_b, lag)
        # ラグを考慮した相関係数を計算
        return 0 if data_a.length < 10 || data_b.length < 10
          
        # 両方のデータから価格を抽出
        a_prices = data_a.map { |d| d[:close] }
        b_prices = data_b.map { |d| d[:close] }
          
        # data_aが先行指標の場合、data_bを遅らせる
        min_length = [a_prices.length - lag, b_prices.length - lag].min
        return 0 if min_length < 10
          
        a_values = a_prices[0, min_length]
        b_values = b_prices[lag, min_length]
        # 配列長の確認
        return 0 if a_values.length != b_values.length || a_values.length < 10
      
        # 価格変化率を計算
        a_changes = (1...a_values.length).map { |i| (a_values[i] - a_values[i-1]) / a_values[i-1] }
        b_changes = (1...b_values.length).map { |i| (b_values[i] - b_values[i-1]) / b_values[i-1] }
      
        correlation(a_changes, b_changes)
      end
      
      def correlation(x, y)
        # ピアソン相関係数を計算
        return 0 if x.length != y.length || x.length < 2
      
        n = x.length.to_f
        sum_x = x.sum
        sum_y = y.sum
        sum_xy = x.zip(y).map { |a, b| a * b }.sum
        sum_x2 = x.map { |a| a * a }.sum
        sum_y2 = y.map { |b| b * b }.sum
      
        numerator = n * sum_xy - sum_x * sum_y
        denominator = Math.sqrt((n * sum_x2 - sum_x * sum_x) * (n * sum_y2 - sum_y * sum_y))
          
        return 0 if denominator == 0
        numerator / denominator
      end
      
      def test_trading_strategy(data_a, data_b, data_c, days_back = 100)
        # 過去100営業日の任意の3営業日でのトレーディング戦略をテスト
        return { profit_rate: 0, successful_trades: 0, total_trades: 0 } if data_c.length < days_back + 3
      
        successful_trades = 0
        total_trades = 0
        total_profit = 0.0
      
        # 過去100営業日から3日連続の組み合わせを抽出してテスト
        (0...(days_back-3)).each do |start_idx|
          day1_idx = start_idx
          day2_idx = start_idx + 1  
          day3_idx = start_idx + 2
      
          # 条件チェック: 1日目 < 2日目の終値(A銘柄、B銘柄)
          next if data_a[day2_idx][:close] <= data_a[day1_idx][:close]
          next if data_b[day2_idx][:close] <= data_b[day1_idx][:close]
      
          # 3日目のC銘柄で寄り付き買い(始値)、引け売り(終値)
          buy_price = data_c[day3_idx][:open]   # 3日目始値で購入
          sell_price = data_c[day3_idx][:close] # 3日目終値で売却
      
          profit = (sell_price - buy_price) / buy_price
          total_profit += profit
          total_trades += 1
            
          successful_trades += 1 if profit > 0
        end
      
        return { profit_rate: 0, successful_trades: 0, total_trades: 0 } if total_trades == 0
      
        {
          profit_rate: total_profit / total_trades,
          successful_trades: successful_trades,
          total_trades: total_trades,
          success_rate: successful_trades.to_f / total_trades
        }
      end
      
      def find_leading_lagging_combinations
        codes = get_stock_codes
        puts "分析開始: #{codes.length} 銘柄"
      
        results = []
        total_combinations = 0
      
        # 全ての3銘柄の組み合わせをテスト(計算量制限のため最初の30銘柄のみ)
        test_codes = codes[0...30]
          
        test_codes.each_with_index do |code_a_info, i|
          code_a = code_a_info['code']
          data_a = get_stock_data(code_a)
          next if data_a.length < 120
      
          test_codes.each_with_index do |code_b_info, j|
            next if j <= i
            code_b = code_b_info['code']
            data_b = get_stock_data(code_b)
            next if data_b.length < 120
      
            test_codes.each_with_index do |code_c_info, k|
              next if i == k || j == k
              code_c = code_c_info['code']
              data_c = get_stock_data(code_c)
              next if data_c.length < 120
      
              total_combinations += 1
              puts "処理中: #{total_combinations} (A:#{code_a}, B:#{code_b}, C:#{code_c})" if total_combinations % 100 == 0
      
              # A->C, B->Cの遅行相関を計算(1-5日のラグ)
              max_correlation_ac = 0
              best_lag_ac = 0
              (1..5).each do |lag|
                corr = calculate_correlation(data_a, data_c, lag)
                if corr.abs > max_correlation_ac.abs
                  max_correlation_ac = corr
                  best_lag_ac = lag
                end
              end
      
              max_correlation_bc = 0
              best_lag_bc = 0
              (1..5).each do |lag|
                corr = calculate_correlation(data_b, data_c, lag)
                if corr.abs > max_correlation_bc.abs
                  max_correlation_bc = corr
                  best_lag_bc = lag
                end
              end
      
              # 両方とも正の相関が0.3以上の場合のみトレーディング戦略をテスト
              if max_correlation_ac > 0.3 && max_correlation_bc > 0.3
                strategy_result = test_trading_strategy(data_a, data_b, data_c)
                  
                if strategy_result[:success_rate] > 0.5 && strategy_result[:total_trades] > 10
                  results << {
                    code_a: code_a,
                    code_b: code_b,
                    code_c: code_c,
                    name_a: code_a_info['company'],
                    name_b: code_b_info['company'],
                    name_c: code_c_info['company'],
                    correlation_ac: max_correlation_ac,
                    correlation_bc: max_correlation_bc,
                    lag_ac: best_lag_ac,
                    lag_bc: best_lag_bc,
                    profit_rate: strategy_result[:profit_rate],
                    success_rate: strategy_result[:success_rate],
                    total_trades: strategy_result[:total_trades]
                  }
                end
              end
            end
          end
        end
      
        puts "分析完了: #{total_combinations} 組み合わせを検証"
        results.sort_by { |r| -r[:success_rate] }
      end
      
      def print_results(results)
        puts "\n=== 分析結果 ==="
        puts "条件に合致する銘柄組み合わせ: #{results.length}"
          
        results[0...10].each_with_index do |result, idx|
          puts "\n#{idx + 1}. 組み合わせ"
          puts "  先行銘柄A: #{result[:code_a]} (#{result[:name_a]}) - 遅行相関: #{result[:correlation_ac].round(3)} (#{result[:lag_ac]}日遅れ)"
          puts "  先行銘柄B: #{result[:code_b]} (#{result[:name_b]}) - 遅行相関: #{result[:correlation_bc].round(3)} (#{result[:lag_bc]}日遅れ)"
          puts "  遅行銘柄C: #{result[:code_c]} (#{result[:name_c]})"
          puts "  戦略成功率: #{(result[:success_rate] * 100).round(1)}%"
          puts "  平均利益率: #{(result[:profit_rate] * 100).round(2)}%"
          puts "  総取引回数: #{result[:total_trades]}"
        end
      end
      
      def close_connections
        @yatoday_db.close
        @crawling_db.close
      end
    end
      
    # メイン実行
    if __FILE__ == $0
      analyzer = StockAnalyzer.new
        
      begin
        results = analyzer.find_leading_lagging_combinations
        analyzer.print_results(results)
      ensure
        analyzer.close_connections
      end
    end
    
  • 結果(抜粋)

    約1.2万の組み合わせの中で、相関係数が0.3以上で勝率がプラスの組み合わせは43ペアしかない。しかも、勝率がプラスでも利益が出るとは限らない。(e.g. 2番目の組み合わせ)後述する用にC銘柄の仕掛けを1日遅らせると組み合わせTOP10の中に平均利益率がマイナスとなる事は無い。

    === 分析結果 ===
    条件に合致する銘柄組み合わせ: 43
      
    1. 組み合わせ
      先行銘柄A: 1377 ((株)サカタのタネ) - 遅行相関: 0.36 (4日遅れ)
      先行銘柄B: 1417 ((株)ミライト・ワン) - 遅行相関: 0.329 (4日遅れ)
      遅行銘柄C: 1382 ((株)ホーブ)
      戦略成功率: 57.7%
      平均利益率: 0.03%
      総取引回数: 26
      
    2. 組み合わせ
      先行銘柄A: 1417 ((株)ミライト・ワン) - 遅行相関: 0.329 (4日遅れ)
      先行銘柄B: 1431 ((株)Lib Work) - 遅行相関: 0.321 (4日遅れ)
      遅行銘柄C: 1382 ((株)ホーブ)
      戦略成功率: 57.7%
      平均利益率: -0.1%
      総取引回数: 26
      
    3. 組み合わせ
      先行銘柄A: 1332 ((株)ニッスイ) - 遅行相関: 0.489 (4日遅れ)
      先行銘柄B: 138A (光フードサービス(株)) - 遅行相関: 0.315 (4日遅れ)
      遅行銘柄C: 1382 ((株)ホーブ)
      戦略成功率: 57.1%
      平均利益率: 0.23%
      総取引回数: 28
    
  • 結果2

    ルール7を次の様に小変更(day3 -> day4)すれば平均利益率がマイナスになることがなくなる。 A銘柄day2終値>day1終値かつB銘柄day2終値>day1終値であればC銘柄day4の寄り付きで買いでエントリーしday4の終値でイグジットする。

    === 分析結果 ===
    条件に合致する銘柄組み合わせ: 99
      
    1. 組み合わせ
      先行銘柄A: 1332 ((株)ニッスイ) - 遅行相関: 0.489 (4日遅れ)
      先行銘柄B: 1418 (インターライフホールディングス(株)) - 遅行相関: 0.313 (4日遅れ)
      遅行銘柄C: 1382 ((株)ホーブ)
      戦略成功率: 70.4%
      平均利益率: 0.72%
      総取引回数: 27
      
    2. 組み合わせ
      先行銘柄A: 135A ((株)VRAIN Solution) - 遅行相関: 0.306 (4日遅れ)
      先行銘柄B: 1430 (ファーストコーポレーション(株)) - 遅行相関: 0.496 (4日遅れ)
      遅行銘柄C: 1382 ((株)ホーブ)
      戦略成功率: 70.4%
      平均利益率: 0.75%
      総取引回数: 27
      
    3. 組み合わせ
      先行銘柄A: 137A (Cocolive(株)) - 遅行相関: 0.342 (4日遅れ)
      先行銘柄B: 1430 (ファーストコーポレーション(株)) - 遅行相関: 0.496 (4日遅れ)
      遅行銘柄C: 1382 ((株)ホーブ)
      戦略成功率: 69.0%
      平均利益率: 0.54%
      総取引回数: 29
    
  • 詳細に分析する

    相関係数は総じて小さく、本当にA銘柄に遅れてC銘柄が動き、更にB銘柄に遅れてC銘柄が動くと言えるのか疑問だ。結果2で、成功率も7割を少し超える程度で、戦略として悪い。9割を超える戦略が欲しいところだ。戦略を小変更してみると成功率を上げる事ができるかもしれない。例えば、

    1. 時価総額TOP100の中から銘柄ペアを取り出す。下記コードのLIMITを100にすれば時価総額TOP100が得られる。

      require 'sqlite3'
      LIMIT = 3
      # __FILE__からディレクトリパスを取得し、ファイル名をstock.dbに置き換える
      db_path = File.join(File.dirname(__FILE__), 'yatoday.db')
      # ファイルの存在確認
      unless File.exist?(db_path)
        puts "エラー: データベースファイル '#{db_path}' が見つかりません。"
        exit(1)
      end
      # SQLiteデータベースに接続
      db = SQLite3::Database.new(db_path)
      # db.results_as_hash = true
           
      # SQLクエリを実行し、結果を取得
      result = db.execute("SELECT code, marketcap FROM codeTbl WHERE code > '1000' LIMIT #{LIMIT}")
           
      # puts result.inspect
      # 結果を指定の形式に変換
      result = result.map do |code, marketcap|
        # marketcapから「百万円」を削除し、整数に変換(カンマも削除)
        marketcap_value = marketcap.gsub(/時価総額|百万円|,/,'').to_i
        { code: code, marketcap: marketcap_value }
      end
      # 時価総額が大きい順にソート
      array = result.sort_by { |hash| -hash[:marketcap] }
           
      # 結果を表示(確認用)
      puts array.inspect
           
      # データベース接続を閉じる
      db.close
      
    2. 3銘柄ペアを4銘柄ペアにしてみる

    3. 銘柄ペアを33業種分類の中から選ぶ。例えば陸運から選ぶとか、電気機器から選ぶとか。

  • 補足

    1. コードはClaude Desktopに作ってもらった。Claudeに最初に与えたPromptは次の通り

      図のように日本株約4,000銘柄のチャートを描くために取引所が閉じた後に株価データソースを更新しています。次の様な3銘柄の組み合わせを見つける事は出来ますか?
      1. 銘柄A、Bの動きに遅れてCが動く
      2. 過去100日の任意の3営業日を観測するとする。1日目、2日目、3日目の3日目にCを寄り付きで買いに入り、場が終わる直前に売り取引を行い、利益が出るとする。
      3. 1日目のAの終値より2日目のAの終値が高い。同じく3. 1日目のBの終値より2日目のBの終値が高い。
      アルゴリズムを提示いただく際にはRubyのスクリプトをおねがいします。
      データは下記のSQLite3データベースの中に格納されています。
      /path/to/yatoday.db のcodeTblテーブルに銘柄情報が格納されています。
      /path/to/crawling.db のcode(1301, 130A..., 9997)テーブルにHistorical Dataが格納されています。
      
    2. テーブル構造は次のとおり

      sqlite> .sch codeTbl
      CREATE TABLE codeTbl(code, exchange, company, quote, change, updating, marketcap, feature, category);
      sqlite> select * from  codeTbl limit 3 offset 37;
      code|exchange|company|quote|change|updating|marketcap|feature|category
      1301|東証PRM|()極洋|---|---|---|時価総額56,345百万円|【特色】水産品の貿易、加工、買い付け主力。すしネタに強み。加工食品は業務用が軸。海外加工比率高い|水産・農林業
      130A|東証GRT|()Veritas In Silico|---|---|---|時価総額4,859百万円|【特色】mRNA標的低分子創薬技術を製薬会社に提供。共同創薬研究などによる契約金収入が柱|医薬品
      1332|東証PRM|()ニッスイ|---|---|---|時価総額272,002百万円|【特色】水産大手で加工・商事のほか日本・南米で養殖。国内外で食品事業展開。EPAなどファインも|水産・農林業
      
      sqlite> .sch '1301'
      CREATE TABLE IF NOT EXISTS "1301" (date text, open real, high real, low real, close real, volume real, adj real, primary key(date));
      sqlite> select * from '1301' order by date desc limit 3;
      date|open|high|low|close|volume|adj
      2025-07-29|4705.0|4725.0|4660.0|4665.0|23100.0|4665.0
      2025-07-28|4795.0|4795.0|4720.0|4730.0|37200.0|4730.0
      2025-07-25|4640.0|4830.0|4640.0|4785.0|78300.0|4785.0