Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KagglerがMixSeekを触ってみた

Avatar for Morim Morim
March 25, 2026

 KagglerがMixSeekを触ってみた

第2回 マケデコハッカソン発表会の発表資料

Avatar for Morim

Morim

March 25, 2026
Tweet

Other Decks in Programming

Transcript

  1. 自己紹介 & 本日の発表テーマ プロフィール • 製造業でデータサイエンス職 • Kaggle Expert •

    JQuants API Premiumプランを利用中(機械学 習を用いたトレードを実践) 本日の発表テーマ MixSeek Quants Insightを使って 投資戦略を作ってみた • 6つのLLMチームを競争させ、open2closeの投 資戦略を自動探索 • バックテストによる実用性の検証 • 各モデルの特徴の確認 2 (Mitsuiコンペ: 97th / 1,711チーム)
  2. MixSeek Quant Insightとは: コンペ型AIエージェントでアルファを自動探索 1 データ入力 OHLCV、ファンダメンタルズ、海外 市場、為替等の時系列 データを投入 2

    チーム並列探索 複数のLLMチームが独立にデー タ分析し、シグナル計算ロジック を生成 3 コンペ評価 提出されたシグナルを 秘匿された評価データで スコアリング(リーク防止) 4 反復改善 スコアをフィードバックし、各チー ムがラウンドを重ねて戦略を洗練 主な特徴 • Kaggle Timeseries API方式のリーク防止評価: エージェントは学習データのみアクセス可能。評価はシグナル関数を 秘匿データに適用 • 提出形式は統一されたPython関数: generate_signal() → 実運用にそのまま転用可能 • OSSとして公開(GitHub: mixseek/mixseek-quant-insight) 出所: https://zenn.dev/gamella/articles/a624a7eb2c78f7 3
  3. 実施概要 データ分割 区分 期間 用途 Train 2020/1 ~ 2023/12 Train

    Analyzerの検証用 Valid 2024/1 ~ 2024/12 Sub Creatorの検証用 Test 2025/1 ~ 2026/2 Subの評価用 評価方法 • ターゲット : open2close • 評価指標  : 日ごとの順位相関のSharpe Ratio • コンペ終了後にバックテストも実施 5 (イニシャル500万円、上位5銘柄ロング & 下位 5銘柄ショート) • ユニバースはTOPIX500銘柄。 • データ期間は2020年1月~2026年2月(約6年分)。 • エージェントチームは6チームを設定 • min_rounds = 4 | max_rounds = 10 | timeout = 7,200秒
  4. 使用データ ファイル ソース 主な内容 ohlcv.parquet J-Quants API 調整済み四本値、出来高、売買代金 master.parquet J-Quants

    API 企業名、市場、業種コード、 TOPIX規模区分 fin_summary.parquet J-Quants API EPS/BPS/売上等(四半期→日次forward-fill) sector_index.parquet J-Quants API 33業種別指数の終値 short_ratio.parquet J-Quants API 業種別空売り比率 topix_index.parquet J-Quants API TOPIX四本値 investor_types.parquet J-Quants API 投資部門別売買動向(週次 →日次forward-fill) overseas.parquet yfinance S&P500, NASDAQ, 日経先物, VIX forex.parquet yfinance ドル円・ユーロ円 commodities.parquet yfinance WTI原油・金・銅 calendar.parquet ローカル 曜日・月末フラグ・SQ日等 ※ yfinanceのデータが(更新タイミングや信頼性の観点で)実運用で利用可能かは未確認 6
  5. エージェントチームについて チーム リーダーモデル temperature 役割 Team 1 Gemini 3.1 Pro

    0.8 シグナル探索 Team 2 Gemini 3 Flash 0.8 シグナル探索 Team 3 GPT 5.4 (Chat Completions API) (設定不可) シグナル探索 Team 4 GPT 5.4 (Response API) (設定不可) シグナル探索 Team 5 Claude Opus 4.6 0.8 シグナル探索 Team 6 Claude Sonnet 4.6 0.8 シグナル探索 共通エージェント エージェント モデル temperature 役割 Train Analyzer Gemini 3.1 Flash-Lite 0.1 学習データの分析・コード実行 Submission Creator Gemini 3.1 Flash-Lite 0.0 シグナル関数の生成・提出 7
  6. 各チームの結果 チーム コンペ結果 バックテスト結果 スコア ラウンド数 総リターン Sharpe MaxDD Gemini

    3.1 Pro 0.23 10(完走) +40.78% 0.994 6.36% Gemini 3 Flash 0.21 10(完走) +31.82% 0.693 12.78% Claude Sonnet 4.6 0.18 4(打ち切り) -5.92% -0.071 24.46% Claude Opus 4.6 0.16 1(エラー) +11.35% 0.247 20.42% GPT 5.4 (Res API) 0.13 5(打ち切り) -16.90% -0.253 36.71% GPT 5.4 (Chat API) 0.13 8(打ち切り) -20.91% -0.515 31.88% • Gemini 3.1 Proが最高スコア0.23を達成。Sharpe Ratio 0.994、MaxDD 6.36%と比較的安定した成績 • GPT 5.4はスコア0.13で頭打ち。シンプルなリバーサルから脱却できず 10
  7. 優勝チームの戦略(Gemini 3.1 Pro) Factor Weight 方向 説明 EP +1.0 Long

    予想EPS / close(益回り) BP +0.5 Long BPS / close(簿価比率) ROE +0.4 Long 純利益 / 純資産 EPS_Growth +0.6 Long 予想EPS 20日変化率 Overnight +0.4 Long 翌日オーバーナイトリターン Mom1M +0.2 Long 1ヶ月モメンタム ShortRatio +0.2 Long 業種別空売り比率 SectorMom +0.2 Long 業種指数リターン O2C -0.8 Short 当日open→closeリターン(逆張り) TurnoverRev -0.4 Short 出来高調整リバーサル Rev1W -0.6 Short 1週間リバーサル Amihud -0.5 Short Amihud非流動性 後処理: ボラティリティスケーリング(20日σで除算) → 日次パーセンタイルランク 11
  8. 優勝チームのシグナル算出コード additional_data = { 'master': master, 'fin_summary': fin_summary, 'short_ratio': short_ratio,

    'sector_index': sector_index, } FACTOR_COLS = ['EP', 'BP', 'ROE', 'EPS_Growth', 'Overnight', 'Mom1M', 'ShortRatio', 'SectorMom', 'O2C', 'TurnoverRev', 'Rev1W', 'Amihud'] WEIGHTS = { 'EP': 1.0, 'BP': 0.5, 'ROE': 0.4, 'EPS_Growth': 0.6, 'Overnight': 0.4, 'Mom1M': 0.2, 'ShortRatio': 0.2, 'SectorMom': 0.2, 'O2C': -0.8, 'TurnoverRev': -0.4, 'Rev1W': -0.6, 'Amihud': -0.5 } def get_z(s): return s.rank(pct=True) - 0.5 def generate_signal_with_components( ohlcv: pl.DataFrame, additional_data: dict[str, pl.DataFrame] ) -> pd.DataFrame: df = ohlcv.to_pandas() master = additional_data['master'].to_pandas() fin_summary = additional_data['fin_summary'].to_pandas() short_ratio = additional_data['short_ratio'].to_pandas() sector_index = additional_data['sector_index'].to_pandas() # --- masterの結合 --- if 'datetime' in master.columns: df = df.sort_values(['datetime', 'symbol']) master = master.sort_values(['datetime', 'symbol']) df = pd.merge_asof( df, master[['datetime', 'symbol', 'sector33_code']], on='datetime', by='symbol', direction='backward' ) else: df = pd.merge(df, master[['symbol', 'sector33_code']], on='symbol', how='left') # --- fin_summaryの結合 --- df = pd.merge( df, fin_summary[['datetime', 'symbol', 'feps', 'bps', 'net_profit', 'equity']], on=['datetime', 'symbol'], how='left' ) # --- short_ratio / sector_index をロング形式へ変換して結合 --- sr_cols = [c for c in short_ratio.columns if c.startswith('short_ratio_')] sr_long = short_ratio.melt(id_vars=['datetime'], value_vars=sr_cols, var_name='sector33_code', value_name='ShortRatio') sr_long['sector33_code'] = sr_long['sector33_code'] \ .str.replace('short_ratio_', '').astype(int) si_cols = [c for c in sector_index.columns if c.endswith('_return')] si_long = sector_index.melt(id_vars=['datetime'], value_vars=si_cols, var_name='sector33_code', value_name='SectorMom') si_long['sector33_code'] = si_long['sector33_code'] \ .str.replace('sector_','').str.replace('_return','').astype(int) df['sector33_code'] = df['sector33_code'].fillna(-1).astype(int) df = pd.merge(df, sr_long, on=['datetime','sector33_code'], how='left') df = pd.merge(df, si_long, on=['datetime','sector33_code'], how='left') # --- 特徴量計算 --- df = df.sort_values(['symbol', 'datetime']).reset_index(drop=True) df['prev_close'] = df.groupby('symbol')['close'].shift(1) df['daily_ret'] = df['close']/df['prev_close'].replace(0,np.nan) - 1 df['EP'] = df['feps'] / df['close'].replace(0, np.nan) df['BP'] = df['bps'] / df['close'].replace(0, np.nan) df['ROE'] = df['net_profit']/df['equity'].abs().replace(0, np.nan) feps_s20 = df.groupby('symbol')['feps'].shift(20) df['EPS_Growth'] = (df['feps']-feps_s20)/feps_s20.abs().replace(0,np.nan) df['O2C'] = df['close'] / df['open'].replace(0, np.nan) - 1 df['SMA20_to'] = df.groupby('symbol')['turnover'] \ .transform(lambda x: x.rolling(20, min_periods=1).mean()) df['TurnoverRev'] = df['O2C']*(df['turnover']/df['SMA20_to'].replace(0,np.nan)) df['Rev1W'] = df['close']/df.groupby('symbol')['close'].shift(5) - 1 df['Mom1M'] = df['close']/df.groupby('symbol')['close'].shift(20) - 1 df['Overnight'] = df['open']/df['prev_close'].replace(0, np.nan) - 1 df['amihud_raw'] = df['daily_ret'].abs()/df['turnover'].replace(0,np.nan) df['Amihud'] = df.groupby('symbol')['amihud_raw'] \ .transform(lambda x: x.rolling(20, min_periods=1).mean()) # --- IC加重合成 + ボラスケーリング + 最終シグナル --- df['composite'] = 0.0 for col, w in WEIGHTS.items(): if col in df.columns: df[col] = df[col].replace([np.inf, -np.inf], np.nan) z = df.groupby('datetime')[col].transform(get_z) df['composite'] += z.fillna(0) * w df['volatility'] = df.groupby('symbol')['daily_ret'] \ .transform(lambda x: x.rolling(20, min_periods=5).std()) med = df.groupby('datetime')['volatility'].transform('median') df['volatility'] = df['volatility'].fillna(med).replace(0,np.nan).fillna(1.0) df['scaled_composite'] = df['composite'] / df['volatility'] df['signal'] = df.groupby('datetime')['scaled_composite'] \ .rank(pct=True).fillna(0.5) return df[['datetime','symbol','signal']] 13
  9. 1位 Gemini 3.1 Pro(スコア : 0.23) • バリュー系: EP(+1.0), BP(+0.5),

    ROE(+0.4) • 成長: EPS 20日変化率(+0.6) • モメンタム: 1ヶ月Mom(+0.2), Overnight(+0.4) • セクター: 業種空売り比率( +0.2), 業種指数リターン (+0.2) • リバーサル: O2C(-0.8), Rev1W(-0.6), TurnoverRev (-0.4) • 流動性: Amihud非流動性(-0.5) • 後処理: ボラティリティスケーリング( 20日σ) 15 12ファクター IC加重モデル ※P.11, P12に詳細 スコア: 0.23 (Round 9)  着実にスコアを伸ばしながら10Rを完走していた。
  10. 2位 Gemini 3 Flash(スコア: 0.21) 16 • 日中リターン逆張り( weight: 1.0)

    • オーバーナイトリターン逆張り( weight: 0.5) • 20日MA乖離の逆張り(weight: 0.3) • 確信度重み①: ROE(財務クオリティ) • 確信度重み②: 異常出来高(Turnover / 20日平均) • 確信度重み③: 低ボラティリティ( 1/vol_20) • 後処理: Sector17中立化 → パーセンタイルランク マルチTFリバーサル + 確信度重み スコア: 0.21 (Round 10)  着実にスコアを伸ばしながら10Rを完走していた。
  11. 3位 Claude Sonnet 4.6(スコア: 0.18) 17 • vol_ratio = volume

    / rolling(20).mean(volume)、 clip[0.1, 10] • sector17×日付グループ内で vol_ratioをpct_rank • raw_signal = (1 - O2C rank) × vol_rank_in_sector • 日付グループ内で raw_signalをpct_rank → NaN補完 (0.5) ※R1とR4で同一ロジック(スコア 0.18で打ち切り) 業種内相対出来高リバーサル スコア: 0.18 (Round 4)  1R目のスコアは1位。TAとSCの呼び出しも多かった。
  12. 4位 Claude Opus 4.6(スコア: 0.16) 18 • reversal_1d: -1 ×

    O2C rank • reversal_2d: -1 × 2日MA(O2C rank) • reversal_5d: -1 × 5日MA(O2C rank) • volume_shock: -1 × (vol/20日平均) × O2C rank • low_vol: -1 × 20日std(O2C rank) • high_low_range: -1 × (high-low)/close × O2C rank • 後処理: 1%/99% winsorize → ランク正規化 → nanmean合成 ※ R2開始時にエラー終了、 1ラウンドのみ 6シグナル等ウェイト合成( Combo L+) スコア: 0.16 (Round 1)  1R目のTAとSCの呼び出し回数が最も多かった。
  13. 5位 GPT 5.4 Response API(スコア: 0.13) 19 • q =

    open2close_return_rank • signal = -(q - 0.5) ※全5ラウンドでスコア変わらず終了 シンプル短期リバーサル スコア: 0.13 (Round 5)  ラウンドは重ねるも、シンプルなリバーサルから脱却できず。
  14. 5位 GPT 5.4 Chat Completion API(スコア: 0.13) 20 • x

    = open2close_return_rank • sector_mean_x = 当日のsector33別平均x • signal_raw = -(x - sector_mean_x)(sector33中立化) • signal = ロバストz-score化(median/MAD、clip±5) • MAD→std→0のフォールバック付き • 全8ラウンドで打ち切り ※SCを呼ばずリーダーがコードを書くパターン多い Sector33中立化リバーサル + ロバスト z-score スコア: 0.13 (Round 8)  ラウンドは重ねるも、シンプルなリバーサルから脱却できず。
  15. まとめ • 今回の検証ではGemini系モデルが最も好成績、それらしい戦略が得られた。 • 各チームを安定して走らせるためには、適宜挙動を確認し、プロンプトを調整する必要がある。 所感 23 • 思ったよりも大変だった。使用するモデルやプロンプト、データの渡し方など、考慮すべき点が多い。(今後参考に できる活用例が増えることに期待)

    • 0ベースで戦術を作らせるのには限界を感じる。こちらで有望なベースラインやシグナルを事前に与えておくほう が、おもしろい戦術が出てくるのかも。 • 汎化性能の評価が難しい。交差検証結果をコンペの評価に含める等の対策を考えたい。