Tana Gone
Tana Gone
1 min read

Categories

JPXはTOPIX 500銘柄や、TOPIX Core 30銘柄を毎年10月末日に入れ替えている。銘柄リストはJPXの株価指数ラインナップ・ページから辿る事で、PDF形式で入手できる。TOPIX 500銘柄はそれ以外の銘柄と異なり呼値が異なる。だから、模擬売買を行う際にストップロスの値を専用の値を設定しないと損切りが遅れる事になる。

  • 株価指数ラインナップと国内株の売買制度(呼値)

    [株価指数関連 日本取引所グループ](https://www.jpx.co.jp/markets/indices/)  
    [概要 内国株の売買制度 日本取引所グループ](https://www.jpx.co.jp/equities/trading/domestic/index.html)
  • 規模別株価指数・採用銘柄テーブルの作成

    下記のレコードの抜粋はテーブルから抜粋したものである。

    sqlite> select * from foo limit 3;
    code|company|category
    1301|極洋|Small 2
    1332|ニッスイ|Mid400
    1333|マルハニチロ|Mid400
    
  • テーブル作成Rubyコード(codepdf2csv.rb)

    # frozen_string_literal: true
    require 'open-uri'
    require 'pdf-reader'
    require 'sqlite3'
    # JPXのPDFから変換したテキストファイルを整形してCSVライクなデータにする
    # 1. tmp_code.txtを読み込む
    # input_file = ARGV[0] || 'tmp_code.txt'
      
    urlStr = "https://www.jpx.co.jp/markets/indices/line-up/files/mei2_12_size.pdf"
    input_file = URI.open urlStr
    # unless input_file.nil?
    #   puts "エラー: 入力ファイル '#{input_file}' が見つかりません。"
    #   exit 1
    # end
    reader = PDF::Reader.new input_file
    all_lines = []
    reader.pages.each do |page|
      all_lines.concat(page.text.split("\n"))
    end
      
    # 2. RegExp /2.*構成銘柄一覧/ にマッチする行までを削除
    lines = all_lines.drop_while { |line| !line.match?(/2.*構成銘柄一覧/) }
      
    unless lines
      puts "エラー: '構成銘柄一覧' で始まるデータセクションが見つかりませんでした。"
      exit 1
    end
      
    # 複数行に分割されたレコードを結合するためのバッファ
    name_part_buffer = nil
      
    # 各行を処理してテーブルを再構築する
    processed_lines = lines.filter_map do |line|
      # 3. 連続する全角空白、連続する半角スペースは1個の半角スペースへ変換
      cleaned_line = line.gsub(/ +/, ' ').gsub(/\s+/, ' ').strip
      
      # 空行、著作権表示、繰り返されるヘッダーをスキップ
      next if cleaned_line.empty? ||
              cleaned_line.include?('Copyright') ||
              cleaned_line.start_with?('No. コード')
      
      # 行が 'No. コード' パターン(2つの数字)で始まるか確認
      if cleaned_line.match?(/^\d+\s+\d+/)
        # この行には番号とコードが含まれている
        # 完全な行か(つまり、2つの数字以上のパートがあるか)を確認
        if cleaned_line.split(' ').length > 2
          # これは完全な行なので、直接出力できる
          # バッファされた銘柄名は関連がない可能性が高いのでクリアする
          name_part_buffer = nil
          cleaned_line
        else
          # この行には数字しか含まれていないため、バッファされた銘柄名と結合する必要がある
          if name_part_buffer
            # 前の行で銘柄名が見つかっているので、それらを結合する
            full_line = cleaned_line + ' ' + name_part_buffer
            name_part_buffer = nil # 使用後にバッファをクリア
            full_line
          else
            # 先行する銘柄名がない数字のみの行
            # 不完全なレコードとして扱い、破棄する
            nil
          end
        end
      else
        # この行は数字で始まらないので、銘柄名部分
        # 次の数字付きの行を待つためにバッファする
        name_part_buffer = cleaned_line
        nil # この行についてはまだ何も出力しない
      end
    end
      
    # 4. 結果として得られた、クリーンアップおよび再構築された行を出力する
    # これにより、目的の4カラム構造に一致する行が暗黙的にフィルタリングされる
    processed_lines.map! do |e|
      e.gsub(/ (TOPIX)/, ",\\1")
        .gsub(/^([0-9]{,4}) /, "\\1,")
        .gsub(/([0-9A-Z]{4}) /, "\\1,")
    end
    # puts processed_lines.join("\n") # No. code, company, old, category
    # 1,1301,極洋,TOPIX,Small 2,TOPIX,Small 2
    db = SQLite3::Database.new 'sizeBased.db'
      
    processed_lines.each do |e|
      table = "foo"
      db.execute <<-SQL
        CREATE TABLE IF NOT EXISTS '#{table}' (
          code TEXT,
          company TEXT,
          category TEXT
        );
      SQL
      ar = e.split ','; br = []; br << ar[1] << ar[2] << ar[6]
      db.execute("INSERT INTO '#{table}' VALUES (?, ?, ?)", br)
    end
      
    db.close
    # Fri, 22 Aug 2025 15:06:25 +0900