1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python3ではじめるシステムトレード:突然現れた巨大ヘッジファンドジェーンストリート

1
Last updated at Posted at 2026-07-29


おすすめビデオ(残念ながらどれも英語です。)

How a Secretive Trading Empire Is Taking Over Wall Street

A Look Inside Jane Street’s Internship Program 2026

metav3rse:Jane Street is one of the most advanced trading firms in the world.

Jane Street: The Secret Trillion-Dollar Ghost Corporation | Financial Crime Documentary

関連記事

上記の英語のビデオを英語で書き下ろし、翻訳し内容を付け加えたものです。

Python3ではじめるシステムトレード:Jane Street 150億ドル損失
Python3ではじめるシステムトレード:Jane Street - Ocamlを動かしてみよう


2026/8/11に「 突然現れた巨大ヘッジファンドジェーンストリート」と題して勉強会をしました。
2026/8/27にこの勉強会の続きをします。ぜひご参加ください。
python3ではじめるシステムトレード: 突然現れた巨大ヘッジファンドジェーンストリート(2)

ジェーン・ストリート

Jane Street Groupは、顧客から資金を集めて運用報酬を得る通常のヘッジファンドではない。中心的な実態は、自社資本を用いて世界各地の市場で取引する自己勘定取引会社であり、同時に、規制下の証券会社を通じて顧客や取引所に流動性を供給するマーケットメーカーでもある。

同社の競争力は、単純な高速取引だけではなく、
$
\text{技術基盤}
+\text{市場横断的な価格評価}
+\text{巨額の自己資本}
+\text{リスク引受能力}
+\text{執行能力}
$
の組合せにある。
 2025年の純トレーディング収益は報道ベースで約396億ドル、従業員数は約3,500人とされる。これは、従業員数 数十万人の大手銀行に匹敵、またはそれを上回るトレーディング収益を、極めて少人数で生み出していることを意味する。
Jane Streetは、単なる高速取引会社でも、通常のヘッジファンドでもない。その本質は、世界の複数市場を一つの巨大な価格体系として捉え、自己資本、計算能力、執行技術、リスク管理を用いて価格差を処理する市場横断型ディーラーである。同社の活動は通常、

  • 流動性を供給する
  • 価格差を縮小する
  • 情報を市場間で伝達する
  • 顧客の即時執行を可能にする
    という社会的機能を持つ。しかし、巨大な派生商品市場と相対的に薄い現物市場が組み合わされると、その能力は、
  • 価格差を発見する能力
  • 価格を形成する能力
  • 参照価格に作用する能力

へと連続的に変化する。
 インド事件の本質は、裁定取引と市場操作の境界が、取引形式ではなく、参照価格への作用を利益獲得の手段として利用したかどうかによって決まるという点にある。したがって、本件は一企業の不正疑惑にとどまらない。指数、オプション満期、現物市場の流動性、マーケットメーカーの資本力という現代市場の構造的問題を示す事例である。

1.創業者、設立経緯、組織拡大の歴史

Jane Streetは2000年、少人数のトレーダーと技術者によってニューヨークで設立された。公式資料は創業者の氏名を明示していないが、複数の報道・業界資料によれば、中心人物はTim Reynolds、Robert “Rob” Granieri、Marc Gerstein、Michael Jenkinsらである。GranieriらはSusquehanna International Groupでの取引経験を持ち、GersteinはIBM出身の技術者だったとされる。

当初の主要市場は、米国預託証券、すなわちADRとオプションだった。その後、ETF市場の拡大とともに、ETFと構成銘柄、先物、通貨、債券との価格差を処理する能力を高めた。

ETFは単一商品の高速売買ではなく、多数の構成資産を同時に評価する必要がある。このため、ETF市場はJane Streetの

  • 市場横断的な価格評価
  • バスケット取引
  • 在庫管理
  • 執行アルゴリズム
  • 大規模なリスク管理

という能力に適していた。
同社は明確なCEOを置く一般的な階層組織とは異なり、経営委員会やリスク委員会を中心とする共同経営的な構造を採用してきたと報じられている。2024年時点では約40人の持分保有者が約240億ドル相当の同社持分を保有していたとされる。
現在はニューヨーク、ロンドン、香港、アムステルダム、シンガポールを主要拠点とし、45か国、200以上の取引施設で活動する。公式には従業員数は3,000人超、最近の報道では約3,500人とされる。

2.ヘッジファンド、自己勘定取引会社、マーケットメーカーのどれか

Jane Streetの最も正確な分類は、

グローバルな自己勘定取引会社を中核とし、規制対象の証券会社を通じてマーケットメイクと顧客取引を行う金融グループ
である。

分析水準 分類
グループ全体 自己勘定取引会社
取引所における機能 マーケットメーカー、流動性供給者
顧客取引 ディーラー、ブローカー・ディーラー
運用会社との比較 ヘッジファンドとは異なる

3.収益、資本規模、従業員数、取扱市場

報道されている純トレーディング収益は、次のように急拡大している。

純トレーディング収益
2023年 約106億ドル
2024年 約205億ドル
2025年 約396億ドル
2026年第1四半期 約161億ドル

S&Pは同社の資本力と利益留保を高く評価する一方、短期市場性資金への依存、急速なバランスシート拡大、規制リスクを信用上の制約としている。2025年7月にはインド事件を受けて見通しが修正されたが、格付けはBBに据え置かれた。
取扱対象は、ETF、個別株式、国債・社債、先物、オプション、外国為替、コモディティ、暗号資産などに及ぶ。2025年には顧客との債券取引が9,000億ドルを超え、約25,000銘柄の債券と約3,800のオプション原資産について価格提示を行ったとされる。

4.クオンツ手法、価格形成、流動性供給、裁定取引

4.1 単純な超高速取引会社ではない

Jane Streetは低遅延技術を重視するが、XTX Marketsのような完全自動化型の価格予測会社とは若干異なる。
同社の公式説明は、人間の判断と自動化された取引システムを組み合わせる点を強調している。市場構造の変化、政策発表、危機、市場参加者自身の行動によってデータ生成過程が変化するため、過去データだけから固定的なモデルを構築することには限界があるという考え方である。
推定価格は、概念的には次のように表せる。
$
\widehat P_{i,t}
=f\left(
P_{\text{関連市場}},
P_{\text{先物}},
P_{\text{オプション}},
\text{金利},
\text{配当},
\text{為替},
\text{注文板},
\text{在庫},
\text{市場状態}
\right)
$
提示するビッド・アスクは、単なる予測値の上下に固定幅を加えたものではない。
$
\text{Spread}
=\text{逆選択費用}
+\text{在庫リスク}
+\text{ヘッジ費用}
$
$+\text{価格変動リスク}
+\text{資本コスト}
+\text{取引所・清算費用}
$
大量の取引を通じて小さな価格差を繰り返し獲得しながら、在庫を市場間で相殺する。
4.2 技術基盤
Jane Streetは大部分のシステムをOCamlで内製し、一部ではC、C++、FPGAを利用する。ネットワーク・カーネルを迂回する通信、専用ハードウェア、取引所との低遅延接続を構築している。機械学習用には数万台規模の高性能GPU、エクサバイト級のデータ基盤を利用するとしている。
ただし、低遅延は競争優位の一部分にすぎない。より重要なのは、ある市場で発生した情報を別市場の価格へ即座に反映し、複数資産を一体として管理できることである。

5.ETF、株式、債券、先物、オプションの位置づけ

5.1 ETF

ETFはJane Streetの中核事業である。
ETF価格を$P_{\mathrm{ETF}}$、構成資産バスケットの価値を$NAV$とすると、
$
B_t=P_{\mathrm{ETF},t}-NAV_t
$
が十分に正ならETFを売り、構成資産を買う。十分に負ならETFを買い、構成資産を売る。指定参加者として設定・解約を利用できれば、乖離の収束を直接実現できる。
もっとも、実際には海外資産、休場中の市場、為替、先物、税、配当、資金調達、設定・解約費用を考慮した「推定NAV」を用いる。
5.2 株式

株式は個別銘柄のマーケットメイクに加え、ETFヘッジ、指数裁定、オプション・ヘッジの手段となる。Jane Streetは2023年に北米株式取引の約10%を占めたと報じられている。
5.3 債券

債券市場では、個別債券の流動性が低く、価格が分断されやすい。国債、金利スワップ、CDS、債券ETF、同一発行体の他年限を用いて相対価値を推定し、顧客に即時価格を提示する。
ここでは単純な取引速度より、巨大な在庫を一時的に保有できる資本力と、ヘッジ市場へのアクセスが重要になる。
5.4 先物

先物は、指数・金利・商品・外国為替の方向性リスクを効率的に調整する主要なヘッジ手段である。
指数先物の理論価格は概ね、
$
F_t=S_t e^{(r-q)(T-t)}
$
で表される。$r$は資金調達金利、$q$は配当利回りである。
5.5 オプション

オプションは、ボラティリティ、歪度、相関、ジャンプ・リスクを取引する市場である。Jane Streetにとっては、単なる方向性取引ではなく、現物、先物、ETFとの関係を含む多次元的な相対価値取引となる。
オプション・ポートフォリオの微小な損益は、
$
dV
\approx
\Delta dS
+\frac12\Gamma(dS)^2
+\mathcal V d\sigma
+\Theta dt
$
と分解できる。
特に満期直前には、権利行使価格付近でガンマが大きくなり、指数のわずかな変化が損益を大きく変える。

6.主要競合との比較

会社 主な特徴 Jane Streetとの差
XTX Markets 機械学習中心の完全電子的マーケットメイク。約300人で日次約2,500億ドルを取引 Jane Streetよりモデル主導・自動化の色彩が強い。特にFXと現物株式で強い
Citadel Securities 米国株式、オプション、個人投資家注文、機関投資家取引で巨大なシェア 顧客注文執行と米国市場インフラへの組込みがJane Street以上に強い
Optiver オプションとボラティリティ取引を起源とする老舗マーケットメーカー デリバティブ・マーケットメイクの伝統が強い
IMC オプション、株式、ETFを中心とする電子マーケットメーカー Jane Streetと近いが、Jane Streetは債券・ETF・顧客取引を含む市場横断性がより顕著
Jane Street ETFを中核とするクロスアセット取引、人間判断と自動化、巨額自己資本 市場、商品、取引期間を横断する総合性が特徴
  • XTXは約300人で日次約2,500億ドルの取引を処理し、機械学習による価格予測を前面に出しています。
  • Citadel Securitiesは米国株式取引の約25%、日次約6,520億ドルを処理するとされ、米国株式・オプション市場で特に大きな存在です。
  • OptiverとIMCは、オプション・マーケットメイクを起源とする点でJane Streetと近い。
  • IMCは近年、インド、中国、外国為替、商品デリバティブへ事業を拡大している。

会計上の収益を比較する場合は注意が必要である。各社とも非上場企業であり、地域法人、純額・総額、顧客取引の範囲が統一されていません。

7. 裁定取引を可能とする構造

基本構造

親会社はJane Street Group, LLC(デラウェア州)で、その下に各法域の規制に対応した子会社が並びます。

Jane Street Capital, LLC(JSC)

米国ではJane Street Capital, LLC(JSC)が中核です。SECにブローカー・ディーラーとして登録され、FINRA加盟。自己勘定で取引し、BATS、CBOE、CME、NASDAQ、NYSE Arca、ICE Futures U.S. などでマーケットメイカーを務めます。さらにその100%子会社としてJSCC Limited(ケイマン諸島、無規制法人)があります。

Jane Street Execution Services, LLC(JSES)

米国にはもう一つJane Street Execution Services, LLC(JSES)があり、これが構造上きわめて重要です。第三者のブローカー・ディーラーや機関投資家に対してエージェンシー・ベースのみでブローカレッジを提供します。第三者の注文は、関連会社が提供する反対側の注文に付け合わせる形で執行されるのが一般的で、加えて関連会社間の証券取引も仲介します。

その他

海外はJane Street Financial Limited(英国FCA)、Jane Street Netherlands B.V.(オランダAFM)、Jane Street Hong Kong Limited(香港SFC)、Jane Street Singapore Pte Ltdという配置です。

裁定との関係:3つの機能

第一に、取引所メンバーシップの確保です。ETF裁定には原資産バスケットを各取引所で執行する必要があり、JSCの多数の取引所会員資格がその基盤です。認定参加者(AP)としてETFの設定・交換を行うにも、SEC登録ブローカー・ディーラーであることが前提になります。

第二に、顧客フローと自己勘定の接続です。JSESが顧客注文を関連会社の反対注文に付け合わせるという構造は、顧客の売買需要をそのままJSCの在庫調整に流し込む仕組みです。市場で裁定機会を探すのではなく、顧客フローそのものが在庫の出入り口になります。エージェンシーと自己勘定を別法人に分けているのは、この利益相反を規制上整理するためです。

第三に、時間帯とアセットクラスをまたぐポジション移転です。JSESが関連会社間取引を仲介する機能は、ニューヨークで抱えた在庫をロンドンや香港の法人に引き渡す実務を支えます。

インド事案が露呈させた構造

ここが最も示唆的です。SEBIが名指しした4法人は、インド国内のJSI Investments Private LimitedとJSI2 Investments Private Limited、そして外国法人のJane Street Singapore Pte LtdとJane Street Asia Trading Limitedでした。

SEBIの指摘は、インド法人を使うことでインドの規制を「回避」できたという点です。外国ポートフォリオ投資家(FPI)は現物市場での日中ポジションを禁じられているため、国内法人が大口の株式・先物取引を執行し、海外のFPI法人が対応するオプションポジションを扱うという分業になっていました。つまり、法人の使い分けそのものが戦略の構成要素だったという認定です。

SEBIは2023年1月から2025年3月の18回の限月日について、BANKNIFTYとNIFTY 50の操作を認定し、朝方に指数構成銘柄と先物を積極的に買って個別銘柄の出来高の20%超を占め、指数を押し上げた、と記述しています。48.4億ルピー(約5.7億ドル)の没収は同規制当局史上最大です。JSI2については、リスクや規制上の理由でグループ内の取引を配分する損失計上の導管として機能した可能性が指摘されています。

裁定と操作の境界

この事案の本質的な論点は、現物と派生商品の間で同時にポジションを持つこと自体は裁定の定義そのものだという点です。ETFの設定・交換もまさに現物とファンド持分の同時取引です。SEBIが問題視したのは、価格発見を歪める意図と規模——最終取引価格に対する注文の集中度など——であって、クロスマーケット取引の構造そのものではありません。

言い換えると、法人構成は裁定を可能にする中立的なインフラですが、同じインフラが規制の穴を通す手段にもなりうる。前回話した「流動性供給と指数の歪曲の境界線」という論点は、ここに具体的な形で現れています。

8.Jane Streetらしい取引戦略のトイモデル

1. 同一指数ETF間の相対価値

  • SPY
  • IVV
  • VOO

のように、同じS&P 500指数を追跡するETFを比較します。SPYとIVVはそれぞれ公式にS&P 500連動商品として説明されています。
例えばSPYとIVVについて、

$$
\log P_t^{SPY}
=\alpha_t+\beta_t\log P_t^{IVV}+u_t
$$

をローリング推定し、

$$
z_t
=\frac{u_t-\overline u_t}{s_t}
$$

を計算します。

  • $z_t>2$:SPY売り・IVV買い
  • $z_t<-2$:SPY買い・IVV売り
  • $|z_t|<0.25$:手仕舞い

とします。

これは非常に実装しやすい一方、実際の価格差は小さいため、日次終値では取引費用を上回る利益は期待しにくいでしょう。戦略の検証方法を学ぶためのトイモデルとして適しています。

売買判断

例えばSPYとIVVについて、

$$
X_t
=\log P_{\mathrm{SPY},t}-\log P_{\mathrm{IVV},t}=
\log\left(
\frac{P_{\mathrm{SPY},t}}
{P_{\mathrm{IVV},t}}
\right)
$$
を計算し、
$
Z_t
=\frac{
X_t-\overline X_t
}{
s_{X,t}
}
$
とします。$Z_t\geq2$ならば、SPYがIVVに対して相対的に高いため、
$
w_{\mathrm{SPY}}=-0.5,
\qquad
w_{\mathrm{IVV}}=0.5
$
とします。$Z_t\leq-2$ならば、反対に、
$
w_{\mathrm{SPY}}=0.5,
\qquad
w_{\mathrm{IVV}}=-0.5
$
とします。この処理を3組に対して行い、その日に最も$|Z_t|$が大きい組だけを取引します。
 なお、これはETFの設定・解約を利用した厳密な無リスク裁定ではなく、日次終値による相対価値・統計的裁定のバックテストです。実際のETF間裁定には、同時刻のビッド・アスク価格が必要です。

========== バックテスト結果 ==========
観測期間 : 2010-09-09 ~ 2026-07-28
観測日数 : 3,994
取引開始回数 : 300
累積収益率 : 1.50%
年率収益率 : 0.09%
年率ボラティリティ : 0.30%
シャープレシオ : 0.313
最大ドローダウン : -1.72%

image.png
image.png
image.png

2. マグニフィセント7とMAGS間の裁定

MAGSは7社に等ウェイトのエクスポージャーを持ち、四半期ごとに等ウェイトへ戻します。ただし、現物株だけでなくスワップ等も利用するアクティブETFなので、以下は完全複製裁定ではなく、MAGSと推定現物バスケットの統計的裁定です。

中心となる回帰は、

$$
\log P_{\mathrm{MAGS},t}
=\alpha_t
+
\beta_t\log B_t
+
\varepsilon_t
$$

です。$B_t$は、7銘柄を四半期ごとに等ウェイトへ戻して作った合成バスケットです。

$Z_t\geq2$ではMAGSを売って現物バスケットを買い、$Z_t\leq-2$ではMAGSを買って現物バスケットを売ります。

実際のMAGSはスワップやフォワードも利用し、日々ポートフォリオを調整しています。そのため、この残差には市場価格のプレミアム・ディスカウントだけでなく、経費、デリバティブ費用、複製誤差、リバランス日のずれも含まれます。(Roundhill Investments)

1.現物裁定

ETFにおける現物裁定は、ETF価格と、そのETFを構成する現物株バスケットの価値との差を利用します。

ETFの理論価値を、

$$
NAV_t=\sum_{i=1}^{n}q_iP_{i,t}
$$

とします。ここで、$q_i$はETF1口に対応する銘柄$i$の数量です。

ETF価格を$P_{\mathrm{ETF},t}$とすると、乖離は、

$$
D_t=P_{\mathrm{ETF},t}-NAV_t
$$

です。

  • ETFが割高な場合

$$
P_{\mathrm{ETF},t}>NAV_t+\text{取引費用}
$$

ならば、

  1. 構成銘柄を買う
  2. 構成銘柄をETF運用会社へ渡してETFを設定する
  3. 受け取ったETFを市場で売る

という取引を行います。

概念的な利益は、

$$
\Pi
=P_{\mathrm{ETF}}+ NAV +\text{取引費用}
$$

です。

  • ETFが割安な場合

反対に、

  1. ETFを買う
  2. ETFを解約して構成銘柄を受け取る
  3. 構成銘柄を市場で売る

という取引を行います。

現物裁定の重要な点は、ETFの設定・解約によって価格差の収束を直接実現できることです。したがって、理論上は将来の平均回帰を待つ必要がありません。

2.統計的裁定

統計的裁定では、ETFと現物バスケットの間に通常成立している統計的関係を推定します。

例えば、

$$
\log P_{\mathrm{ETF},t}
=\alpha
+
\beta\log B_t
+
\varepsilon_t
$$

とします。

$B_t$は推定した現物バスケット価格、$\varepsilon_t$はETF価格とバスケットの通常の関係からの乖離です。

残差を標準化して、

Z_t=\frac{\varepsilon_t - - \overline{\varepsilon}_t}{s_{\varepsilon,t}}

を計算します。

例えば、

  • $Z_t\geq2$:ETF売り、バスケット買い
  • $Z_t\leq-2$:ETF買い、バスケット売り
  • $Z_t$がゼロ付近:決済

とします。

しかし、統計的裁定では、

$$
\varepsilon_t\longrightarrow0
$$

となることは保証されていません。過去に平均回帰していたという統計的性質を利用しているだけです。

3.両者の本質的な違い

項目 現物裁定 統計的裁定
基準価格 実際のNAV・構成銘柄価値 推定された均衡関係
利益の根拠 同一価値の商品間の価格差 過去の平均回帰性
収束方法 設定・解約で直接収束させる 市場価格の自然な収束を待つ
利益の確実性 費用控除後に固定できれば高い 確率的であり保証されない
主な参加者 指定参加者、マーケットメーカー ヘッジファンド、クオンツ運用者
必要データ リアルタイム価格、正確な構成数量 過去価格、回帰・時系列モデル
主なリスク 執行、資金調達、決済、空売り モデル破綻、構造変化、発散
取引期間 秒から分程度が中心 日、週、場合によっては月

4.現物裁定も完全な無リスクではありません

現実には、現物裁定にも次のリスクがあります。

$
\text{実現利益}
=\text{価格差}- \text{ビッド・アスク}-\text{手数料}-\text{市場インパクト}
$
$$-\text{資金調達費用}-\text{空売り費用}
$$

特に、多数の構成銘柄を同時に取引する間に価格が変化すれば、当初観測した価格差を固定できません。したがって、厳密には、

すべての売買を同時に確定でき、設定・解約まで実行できる場合に、現物裁定に近づきます。

5.MAGSの場合

今回作成したMAGSと7銘柄のコードは、現物裁定ではなく統計的裁定です。理由は次のとおりです。

  • MAGSは単純な7銘柄の現物保有だけではありません
  • スワップなどのデリバティブを利用します
  • 実際の保有数量や日中の調整を完全には再現していません
  • 四半期等ウェイトという近似バスケットを使っています
  • ETFの設定・解約を実行していません

したがって、コードで使っている乖離は、

$$
\text{ETFの市場価格}
-\text{推定した現物バスケット価格}
$$

です。

これは真のプレミアム・ディスカウントだけではなく、

$$
\begin{aligned}
\text{観測残差}
={}&
\text{ETF価格の乖離}
+\text{複製誤差}
&+\text{ウェイト推定誤差}
+\text{デリバティブ評価差}
+\text{経費差}
\end{aligned}
$$

を含みます。したがって、現在のコードは、

MAGSと7銘柄の関係が一時的に通常状態から外れ、その後戻る

という仮説を検証するものです。一方、本当の現物裁定を行うには、ETFの正確な日次構成ファイル、リアルタイムの各銘柄価格、設定・解約単位、現金調整額、取引コスト、そして指定参加者としてのアクセスが必要になります。

========== バックテスト結果 ==========
観測期間 : 2023-04-11 ~ 2026-07-29
観測日数 : 828
ローリング期間 : 60営業日
取引開始回数 : 29
累積収益率 : 2.88%
年率収益率 : 0.87%
年率ボラティリティ : 0.81%
シャープレシオ : 1.065
最大ドローダウン : -0.77%
平均グロス投資比率 : 37.44%
image.png
image.png
image.png
image.png

コード

同一指数ETF間の相対価値

from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False
# ============================================================
# 1. 設定
# ============================================================
FILES = {
    "SPY": Path("spy.csv"),
    "IVV": Path("ivv.csv"),
    "VOO": Path("voo.csv"),
}
WINDOW = 60                  # 平均・標準偏差の推定期間
ENTRY_Z = 2.0                # エントリー閾値
EXIT_Z = 0.25                # 決済閾値
TRANSACTION_COST_BPS = 2.0  # 売買コスト:片道2bp
BORROW_RATE = 0.01           # 空売り調達コスト:年率1%
TRADING_DAYS = 252
# ============================================================
# 2. CSVの読み込み
# ============================================================
def load_price(path: Path, ticker: str) -> pd.Series:
    """
    Date列とAdj CloseまたはClose列を持つCSVを読み込みます。
    Adj Closeが存在する場合は、Adj Closeを優先します。
    """
    df = pd.read_csv(path)
    if "Date" not in df.columns:
        raise ValueError(f"{path} にDate列がありません。")
    df["Date"] = pd.to_datetime(
        df["Date"],
        errors="coerce"
    )
    if "Adj Close" in df.columns:
        price_column = "Adj Close"
    elif "Close" in df.columns:
        price_column = "Close"
    else:
        raise ValueError(
            f"{path} にAdj CloseまたはClose列がありません。"
        )
    price = pd.to_numeric(
        df[price_column],
        errors="coerce"
    )
    series = pd.Series(
        price.to_numpy(),
        index=df["Date"],
        name=ticker,
        dtype=float,
    )
    series = (
        series
        .dropna()
        .sort_index()
    )
    series = series.loc[
        ~series.index.duplicated(keep="last")
    ]
    return series
price_list = []

for ticker, path in FILES.items():
    price_list.append(
        load_price(path, ticker)
    )
prices = pd.concat(
    price_list,
    axis=1,
    join="inner"
).dropna()
if len(prices) <= WINDOW:
    raise ValueError(
        f"共通するデータが不足しています。"
        f"少なくとも{WINDOW + 1}営業日が必要です。"
    )
# ============================================================
# 3. ETFの組合せ
# ============================================================
PAIR_MAP = {
    "SPY/IVV": ("SPY", "IVV"),
    "SPY/VOO": ("SPY", "VOO"),
    "IVV/VOO": ("IVV", "VOO"),
}
# ============================================================
# 4. 対数価格比率
# ============================================================
log_prices = np.log(prices)
spreads = pd.DataFrame(
    index=prices.index
)
z_scores = pd.DataFrame(
    index=prices.index
)
for pair_name, pair in PAIR_MAP.items():
    ticker_a, ticker_b = pair
    # 対数価格比率
    spread = (
        log_prices[ticker_a]
        - log_prices[ticker_b]
    )
    rolling_mean = (
        spread
        .rolling(
            WINDOW,
            min_periods=WINDOW
        )
        .mean()
    )
    rolling_std = (
        spread
        .rolling(
            WINDOW,
            min_periods=WINDOW
        )
        .std(ddof=1)
    )
    spreads[pair_name] = spread
    z_scores[pair_name] = (
        spread - rolling_mean
    ) / rolling_std.replace(0, np.nan)
# ============================================================
# 5. 最も乖離した1組を取引する
# ============================================================
signal_weights = pd.DataFrame(
    0.0,
    index=prices.index,
    columns=prices.columns,
)
signal_pair = pd.Series(
    pd.NA,
    index=prices.index,
    dtype="object",
    name="signal_pair",
)
current_pair = None
entry_sign = 0
for date in prices.index:
    z_today = z_scores.loc[date].dropna()
    # --------------------------------------------------------
    # ポジションを持っていない場合
    # --------------------------------------------------------
    if current_pair is None:
        if not z_today.empty:
            # 絶対値が最も大きい組合せ
            candidate_pair = (
                z_today
                .abs()
                .idxmax()
            )
            candidate_z = z_today[candidate_pair]
            if abs(candidate_z) >= ENTRY_Z:
                current_pair = candidate_pair
                # エントリー時のZスコアの符号
                entry_sign = int(
                    np.sign(candidate_z)
                )
    # --------------------------------------------------------
    # ポジションを保有している場合
    # --------------------------------------------------------
    else:
        current_z = z_scores.at[
            date,
            current_pair
        ]
        # 平均付近に戻るか、
        # 平均を通過した場合に決済
        exit_condition = (
            pd.isna(current_z)
            or abs(current_z) <= EXIT_Z
            or current_z * entry_sign <= 0
        )
        if exit_condition:
            current_pair = None
            entry_sign = 0
    # --------------------------------------------------------
    # 保有するウェイトを設定
    # --------------------------------------------------------
    if current_pair is not None:

        ticker_a, ticker_b = PAIR_MAP[
            current_pair
        ]
        current_z = z_scores.at[
            date,
            current_pair
        ]
        if current_z > 0:
            # A/Bが通常より高い
            # Aを売り、Bを買う
            signal_weights.at[
                date,
                ticker_a
            ] = -0.5
            signal_weights.at[
                date,
                ticker_b
            ] = 0.5
        else:
            # A/Bが通常より低い
            # Aを買い、Bを売る
            signal_weights.at[
                date,
                ticker_a
            ] = 0.5
            signal_weights.at[
                date,
                ticker_b
            ] = -0.5
        signal_pair.at[date] = current_pair
# ============================================================
# 6. シグナルを1日遅らせる
# ============================================================
# 当日の終値で計算したシグナルを
# 翌営業日のポジションとして使用します
weights = (
    signal_weights
    .shift(1)
    .fillna(0.0)
)
active_pair = signal_pair.shift(1)
# ============================================================
# 7. ETFの日次収益率
# ============================================================
returns = (
    prices
    .pct_change(fill_method=None)
    .fillna(0.0)
)
gross_return = (
    weights * returns
).sum(axis=1)
# ============================================================
# 8. 売買回転率と取引コスト
# ============================================================
turnover = (
    weights
    .diff()
    .abs()
    .sum(axis=1)
)
turnover.iloc[0] = (
    weights
    .iloc[0]
    .abs()
    .sum()
)
transaction_cost = (
    turnover
    * TRANSACTION_COST_BPS
    / 10_000
)
# ============================================================
# 9. 空売り調達コスト
# ============================================================
short_notional = (
    -weights.clip(upper=0)
).sum(axis=1)
borrow_cost = (
    short_notional
    * BORROW_RATE
    / TRADING_DAYS
)
# ============================================================
# 10. 戦略収益率
# ============================================================
strategy_return = (
    gross_return
    - transaction_cost
    - borrow_cost
)
equity = (
    1.0 + strategy_return
).cumprod()
running_max = equity.cummax()
drawdown = (
    equity / running_max
    - 1.0
)
# ============================================================
# 11. 結果をまとめる
# ============================================================
result = pd.concat(
    [
        prices,
        spreads.add_prefix("spread_"),
        z_scores.add_prefix("z_"),
        weights.add_prefix("weight_"),
        active_pair.rename("active_pair"),
        gross_return.rename("gross_return"),
        turnover.rename("turnover"),
        transaction_cost.rename(
            "transaction_cost"
        ),
        borrow_cost.rename("borrow_cost"),
        strategy_return.rename(
            "strategy_return"
        ),
        equity.rename("equity"),
        drawdown.rename("drawdown"),
    ],
    axis=1,
)
# ============================================================
# 12. パフォーマンス指標
# ============================================================
number_of_days = len(result)
total_return = (
    equity.iloc[-1] - 1.0
)
annual_return = (
    equity.iloc[-1]
    ** (TRADING_DAYS / number_of_days)
    - 1.0
)
annual_volatility = (
    strategy_return.std(ddof=1)
    * np.sqrt(TRADING_DAYS)
)
if strategy_return.std(ddof=1) > 0:

    sharpe_ratio = (
        strategy_return.mean()
        / strategy_return.std(ddof=1)
        * np.sqrt(TRADING_DAYS)
    )
else:
    sharpe_ratio = np.nan
maximum_drawdown = drawdown.min()
gross_exposure = (
    weights
    .abs()
    .sum(axis=1)
)
entry_flag = (
    (gross_exposure > 0)
    & (
        gross_exposure
        .shift(1)
        .fillna(0)
        == 0
    )
)
number_of_trades = int(
    entry_flag.sum()
)
print("========== バックテスト結果 ==========")
print(
    f"観測期間            : "
    f"{result.index.min().date()}"
    f"{result.index.max().date()}"
)
print( f"観測日数            : "f"{number_of_days:,}")

print( f"取引開始回数        : "f"{number_of_trades:,}")
print( f"累積収益率          : "f"{total_return:.2%}")
print( f"年率収益率          : "f"{annual_return:.2%}")
print( f"年率ボラティリティ  : "f"{annual_volatility:.2%}")
print( f"シャープレシオ      : " f"{sharpe_ratio:.3f}")
print( f"最大ドローダウン    : " f"{maximum_drawdown:.2%}")
# ============================================================
# 13. 基準化価格のグラフ
# ============================================================
normalized_prices = (
    prices / prices.iloc[0]
)
plt.figure(figsize=(4, 2.5))
for ticker in normalized_prices.columns:
    plt.plot(
        normalized_prices.index,
        normalized_prices[ticker],
        label=ticker,
    )
plt.title("SPY・IVV・VOOの基準化価格")
plt.xlabel("Date")
plt.ylabel("Normalized price")
plt.legend()
plt.grid()
plt.show()
# ============================================================
# 14. Zスコアのグラフ
# ============================================================
plt.figure(figsize=(4, 2.5))
for pair_name in z_scores.columns:
    plt.plot(
        z_scores.index,
        z_scores[pair_name],
        label=pair_name,
    )
plt.axhline(
    ENTRY_Z,
    linestyle="--"
)
plt.axhline(
    -ENTRY_Z,
    linestyle="--"
)
plt.axhline(
    EXIT_Z,
    linestyle=":"
)
plt.axhline(
    -EXIT_Z,
    linestyle=":"
)
plt.axhline(0)
plt.title(
    "同一指数ETF間の価格比率Zスコア"
)
plt.xlabel("Date")
plt.ylabel("Z-score")
plt.legend()
plt.grid()
plt.show()
# ============================================================
# 15. 累積資産のグラフ
# ============================================================
plt.figure(figsize=(4, 2.5))
plt.plot(
    equity.index,
    equity
)
plt.title(
    "SPY・IVV・VOO相対価値戦略の累積資産"
)
plt.xlabel("Date")
plt.ylabel("Equity")
plt.grid()
plt.show()
# ============================================================
# 16. CSVに保存
# ============================================================
result.to_csv(
    "spy_ivv_voo_relative_value_result.csv",
    encoding="utf-8-sig",
)

mags vs マグニフィセント7 裁定

from pathlib import Path
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# ============================================================
# 1. グラフの日本語フォント
# ============================================================
plt.rcParams["font.family"] = "Yu Gothic"
plt.rcParams["axes.unicode_minus"] = False
# ============================================================
# 2. 設定
# ============================================================
FILES = {
    "MAGS": Path("mags.csv"),
    "AAPL": Path("aapl.csv"),
    "MSFT": Path("msft.csv"),
    "AMZN": Path("amzn.csv"),
    "GOOGL": Path("googl.csv"),
    "META": Path("meta.csv"),
    "NVDA": Path("nvda.csv"),
    "TSLA": Path("tsla.csv"),
}
STOCK_TICKERS = ["AAPL","MSFT","AMZN","GOOGL","META","NVDA","TSLA",]
WINDOW = 60
ENTRY_Z = 2.0
EXIT_Z = 0.25
TRADING_DAYS = 252
# 片道売買コスト
TRANSACTION_COST_BPS = 2.0
# 空売りコストの仮定
BORROW_RATE_MAGS = 0.01
BORROW_RATE_STOCKS = 0.005
# "dollar":MAGSとバスケットを50%ずつ
# "beta"  :ローリング回帰のβでヘッジ
HEDGE_MODE = "beta"
# βが異常値になった場合の制限
MIN_HEDGE_BETA = 0.50
MAX_HEDGE_BETA = 1.50
# ============================================================
# 3. CSVの読み込み
# ============================================================
def load_price(path: Path, ticker: str) -> pd.Series:
    """
    Date列とAdj CloseまたはClose列を持つCSVを読み込みます。
    Adj Closeがあれば優先して使用します。
    """
    if not path.exists():
        raise FileNotFoundError(
            f"{path} が見つかりません。"
        )
    df = pd.read_csv(path)
    if "Date" not in df.columns:
        raise ValueError(
            f"{path} にDate列がありません。"
        )
    df["Date"] = pd.to_datetime(
        df["Date"],
        errors="coerce",
    )
    if "Adj Close" in df.columns:
        price_column = "Adj Close"
    elif "Close" in df.columns:
        price_column = "Close"
    else:
        raise ValueError(
            f"{path} にAdj CloseまたはClose列がありません。"
        )
    price = pd.to_numeric(
        df[price_column],
        errors="coerce",
    )
    result = pd.Series(
        price.to_numpy(),
        index=df["Date"],
        name=ticker,
        dtype=float,
    )
    result = (
        result
        .dropna()
        .sort_index()
    )
    result = result.loc[
        ~result.index.duplicated(keep="last")
    ]
    if (result <= 0).any():
        raise ValueError(
            f"{ticker}の価格に0以下の値があります。"
        )
    return result
price_series = []
for ticker, path in FILES.items():
    price_series.append(
        load_price(path, ticker)
    )
# 8銘柄すべてにデータが存在する日だけを使用
prices = pd.concat(
    price_series,
    axis=1,
    join="inner",
).dropna()
if len(prices) <= WINDOW + 5:
    raise ValueError(
        f"共通するデータが不足しています。"
        f"少なくとも{WINDOW + 6}営業日程度必要です。"
    )
mags_price = prices["MAGS"]
stock_prices = prices[STOCK_TICKERS]
# ============================================================
# 4. 四半期等ウェイト・バスケットの作成
# ============================================================
def create_quarterly_equal_weight_basket(
    stock_price_data: pd.DataFrame,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
    """
    四半期の境界で7銘柄を等ウェイトに戻します。

    四半期中は売買せず、各銘柄の価格変動に応じて
    ウェイトが自然に変化するものとします。
    戻り値
    ------
    basket_index
        合成バスケット指数
    basket_weights
        各日の取引終了時点における構成ウェイト
    stock_returns
        各銘柄の日次収益率
   """
    stock_returns = (
        stock_price_data
        .pct_change(fill_method=None)
    )
    basket_index = pd.Series(
        np.nan,
        index=stock_price_data.index,
        name="basket_index",
        dtype=float,
    )
    basket_weights = pd.DataFrame(
        np.nan,
        index=stock_price_data.index,
        columns=stock_price_data.columns,
        dtype=float,
    )
    number_of_stocks = stock_price_data.shape[1]
    equal_weights = np.repeat(
        1.0 / number_of_stocks,
        number_of_stocks,
    )
    current_weights = equal_weights.copy()
    basket_index.iloc[0] = 1.0
    basket_weights.iloc[0] = current_weights
    quarters = stock_price_data.index.to_period("Q")
    for i in range(1, len(stock_price_data)):
        # 四半期が変わったとき、等ウェイトに戻します
        if quarters[i] != quarters[i - 1]:
            current_weights = equal_weights.copy()
        daily_returns = (
            stock_returns
            .iloc[i]
            .to_numpy(dtype=float)
        )
        if not np.isfinite(daily_returns).all():
            raise ValueError(
                f"{stock_price_data.index[i]}"
                "構成銘柄収益率に欠損があります。"
            )
        basket_return = float(
            np.dot(
                current_weights,
                daily_returns,
            )
        )
        basket_index.iloc[i] = (
            basket_index.iloc[i - 1]
            * (1.0 + basket_return)
        )
        # 価格変動後のウェイト
        current_weights = (
            current_weights
            * (1.0 + daily_returns)
            / (1.0 + basket_return)
        )
        # 数値誤差を除くため再正規化
        current_weights = (
            current_weights
            / current_weights.sum()
        )
        basket_weights.iloc[i] = current_weights
    return (
        basket_index,
        basket_weights,
        stock_returns,
    )
(
    basket_index,
    basket_weights,
    stock_returns,
) = create_quarterly_equal_weight_basket(
    stock_prices
)
basket_return = (
    basket_index
    .pct_change(fill_method=None)
    .fillna(0.0)
)
# ============================================================
# 5. MAGSと合成バスケットのローリング回帰
# ============================================================
# log(MAGS) = alpha + beta * log(Basket) + residual
y = np.log(mags_price)
x = np.log(basket_index)
rolling_mean_x = (
    x
    .rolling(
        WINDOW,
        min_periods=WINDOW,
    )
    .mean()
)
rolling_mean_y = (
    y
    .rolling(
        WINDOW,
        min_periods=WINDOW,
    )
    .mean()
)
rolling_var_x = (
    x
    .rolling(
        WINDOW,
        min_periods=WINDOW,
    )
    .var(ddof=1)
)
rolling_var_y = (
    y
    .rolling(
        WINDOW,
        min_periods=WINDOW,
    )
    .var(ddof=1)
)
rolling_cov_xy = (
    y
    .rolling(
        WINDOW,
        min_periods=WINDOW,
    )
    .cov(x)
)
beta = (
    rolling_cov_xy
    / rolling_var_x.replace(0, np.nan)
)
beta.name = "beta"
alpha = (
    rolling_mean_y
    - beta * rolling_mean_x
)
alpha.name = "alpha"
spread = (
    y
    - alpha
    - beta * x
)
spread.name = "spread"
# ============================================================
# 6. 回帰残差の標準偏差
# ============================================================
# 中心化平方和を使った回帰残差平方和
#
# SSE = (n - 1) ×
#       {Var(y) - beta × Cov(x, y)}
residual_sse = (
    (WINDOW - 1)
    * (
        rolling_var_y
        - beta * rolling_cov_xy
    )
)
# 数値誤差でわずかに負になる場合に備えます
residual_sse = residual_sse.clip(lower=0)
residual_std = np.sqrt(
    residual_sse
    / (WINDOW - 2)
)
residual_std.name = "residual_std"
z_score = (
    spread
    / residual_std.replace(0, np.nan)
)
z_score.name = "z_score"
# ============================================================
# 7. 売買シグナル
# ============================================================
# position =  1
# MAGS買い、現物バスケット売り
#
# position = -1
# MAGS売り、現物バスケット買い
#
# position =  0
# ポジションなし
signal_position = pd.Series(
    0,
    index=prices.index,
    name="signal_position",
    dtype=int,
)
current_position = 0
for date in prices.index:
    current_z = z_score.at[date]
    if pd.isna(current_z):
        current_position = 0
    elif current_position == 0:
        # MAGSが推定バスケットに対して割高
        if current_z >= ENTRY_Z:
            current_position = -1
        # MAGSが推定バスケットに対して割安
        elif current_z <= -ENTRY_Z:
            current_position = 1
    elif current_position == 1:
        # MAGS割安ポジションの決済
        if current_z >= -EXIT_Z:
            current_position = 0
    elif current_position == -1:
        # MAGS割高ポジションの決済
        if current_z <= EXIT_Z:
            current_position = 0
    signal_position.at[date] = current_position
# ============================================================
# 8. 各資産へのシグナル・ウェイト
# ============================================================
signal_weights = pd.DataFrame(
    0.0,
    index=prices.index,
    columns=prices.columns,
)
for date in prices.index:
    position = signal_position.at[date]
    if position == 0:
        continue
    current_beta = beta.at[date]
    if not np.isfinite(current_beta):
        continue
    hedge_beta = float(
        np.clip(
            abs(current_beta),
            MIN_HEDGE_BETA,
            MAX_HEDGE_BETA,
        )
    )
    if HEDGE_MODE == "dollar":
        # MAGS側50%、バスケット側50%
        mags_gross_weight = 0.5
        basket_gross_weight = 0.5
    elif HEDGE_MODE == "beta":
        # 総グロスを100%に保ちながら
        # 回帰βに応じて配分
        mags_gross_weight = (
            1.0
            / (1.0 + hedge_beta)
        )
        basket_gross_weight = (
            hedge_beta
            / (1.0 + hedge_beta)
        )
    else:
        raise ValueError(
            "HEDGE_MODEは'dollar'または'beta'にしてください。"
        )
    # position = 1ならMAGS買い
    # position = -1ならMAGS売り
    signal_weights.at[
        date,
        "MAGS"
    ] = (
        position
        * mags_gross_weight
    )
    # MAGSと反対方向に現物バスケットを保有
    basket_side = (
        -position
        * basket_gross_weight
    )
    for ticker in STOCK_TICKERS:
        signal_weights.at[
            date,
            ticker
        ] = (
            basket_side
            * basket_weights.at[date, ticker]
        )
# ============================================================
# 9. 先読みを避けるため1営業日遅らせる
# ============================================================
# 当日終値で計算したシグナルを、
# 翌営業日の収益率に適用します
weights = (
    signal_weights
    .shift(1)
    .fillna(0.0)
)
executed_position = (
    signal_position
    .shift(1)
    .fillna(0)
    .astype(int)
)
executed_position.name = "position"
# ============================================================
# 10. 各資産の日次収益率
# ============================================================
asset_returns = (
    prices
    .pct_change(fill_method=None)
    .fillna(0.0)
)
gross_return = (
    weights
    * asset_returns
).sum(axis=1)
gross_return.name = "gross_return"
# ============================================================
# 11. 売買回転率と取引コスト
# ============================================================
turnover = (
    weights
    .diff()
    .abs()
    .sum(axis=1)
)
turnover.iloc[0] = (
    weights
    .iloc[0]
    .abs()
    .sum()
)
turnover.name = "turnover"
transaction_cost = (
    turnover
    * TRANSACTION_COST_BPS
    / 10_000
)
transaction_cost.name = "transaction_cost"
# ============================================================
# 12. 空売りコスト
# ============================================================
annual_borrow_rates = pd.Series(
    {
        "MAGS": BORROW_RATE_MAGS,
        "AAPL": BORROW_RATE_STOCKS,
        "MSFT": BORROW_RATE_STOCKS,
        "AMZN": BORROW_RATE_STOCKS,
        "GOOGL": BORROW_RATE_STOCKS,
        "META": BORROW_RATE_STOCKS,
        "NVDA": BORROW_RATE_STOCKS,
        "TSLA": BORROW_RATE_STOCKS,
    }
)
short_weights = (
    -weights.clip(upper=0)
)
borrow_cost = (
    short_weights
    .mul(
        annual_borrow_rates
        / TRADING_DAYS,
        axis=1,
    )
    .sum(axis=1)
)
borrow_cost.name = "borrow_cost"
# ============================================================
# 13. 戦略収益率
# ============================================================
strategy_return = (
    gross_return
    - transaction_cost
    - borrow_cost
)
strategy_return.name = "strategy_return"
equity = (
    1.0 + strategy_return
).cumprod()
equity.name = "equity"
running_max = equity.cummax()
drawdown = (
    equity
    / running_max
    - 1.0
)
drawdown.name = "drawdown"
# ============================================================
# 14. 結果の整理
# ============================================================
result = pd.concat(
    [
        prices,
        basket_index,
        basket_return.rename("basket_return"),
        basket_weights.add_prefix("basket_weight_"),
        alpha,
        beta,
        spread,
        residual_std,
        z_score,
        signal_position,
        executed_position,
        weights.add_prefix("position_weight_"),
        gross_return,
        turnover,
        transaction_cost,
        borrow_cost,
        strategy_return,
        equity,
        drawdown,
    ],
    axis=1,
)
# ============================================================
# 15. パフォーマンス指標
# ============================================================
number_of_days = len(strategy_return)
total_return = (
    equity.iloc[-1] - 1.0
)
if equity.iloc[-1] > 0:
    annual_return = (
        equity.iloc[-1]
        ** (
            TRADING_DAYS
            / number_of_days
        )
        - 1.0
    )
else:
    annual_return = np.nan
annual_volatility = (
    strategy_return.std(ddof=1)
    * np.sqrt(TRADING_DAYS)
)
if strategy_return.std(ddof=1) > 0:
    sharpe_ratio = (
        strategy_return.mean()
        / strategy_return.std(ddof=1)
        * np.sqrt(TRADING_DAYS)
    )
else:
    sharpe_ratio = np.nan
maximum_drawdown = drawdown.min()
entry_flag = (
    (executed_position != 0)
    & (
        executed_position
        .shift(1)
        .fillna(0)
        == 0
    )
)
number_of_trades = int(
    entry_flag.sum()
)
average_gross_exposure = (
    weights
    .abs()
    .sum(axis=1)
    .mean()
)
print("========== バックテスト結果 ==========")
print(
    f"観測期間              : "
    f"{result.index.min().date()}"
    f"{result.index.max().date()}"
)
print(
    f"観測日数              : "
    f"{number_of_days:,}"
)
print(
    f"ローリング期間        : "
    f"{WINDOW}営業日"
)
print(
    f"取引開始回数          : "
    f"{number_of_trades:,}"
)
print(
    f"累積収益率            : "
    f"{total_return:.2%}"
)
print(
    f"年率収益率            : "
    f"{annual_return:.2%}"
)
print(
    f"年率ボラティリティ    : "
    f"{annual_volatility:.2%}"
)
print(
    f"シャープレシオ        : "
    f"{sharpe_ratio:.3f}"
)
print(
    f"最大ドローダウン      : "
    f"{maximum_drawdown:.2%}"
)
print(
    f"平均グロス投資比率    : "
    f"{average_gross_exposure:.2%}"
)
# ============================================================
# 16. MAGSと合成バスケット
# ============================================================
normalized_mags = (
    mags_price
    / mags_price.iloc[0]
)
plt.figure(figsize=(4, 2.5))
plt.plot(
    normalized_mags.index,
    normalized_mags,
    label="MAGS",
)
plt.plot(
    basket_index.index,
    basket_index,
    label="7銘柄合成バスケット",
)
plt.title(
    "MAGSと四半期等ウェイト合成バスケット"
)
plt.xlabel("Date")
plt.ylabel("基準化価格")
plt.legend()
plt.grid()
plt.show()
# ============================================================
# 17. ローリングβ
# ============================================================
plt.figure(figsize=(4, 2.5))
plt.plot(
    beta.index,
    beta,
)
plt.axhline(
    1.0,
    linestyle="--",
)
plt.title(
    "MAGSと合成バスケットのローリングβ"
)
plt.xlabel("Date")
plt.ylabel("Beta")
plt.grid()
plt.show()
# ============================================================
# 18. 残差のZスコア
# ============================================================
plt.figure(figsize=(4, 2.5))
plt.plot(
    z_score.index,
    z_score,
)
plt.axhline(
    ENTRY_Z,
    linestyle="--",
)
plt.axhline(
    -ENTRY_Z,
    linestyle="--",
)
plt.axhline(
    EXIT_Z,
    linestyle=":",
)
plt.axhline(
    -EXIT_Z,
    linestyle=":",
)
plt.axhline(
    0.0,
)
plt.title(
    "MAGSと合成バスケットの残差Zスコア"
)
plt.xlabel("Date")
plt.ylabel("Z-score")
plt.grid()
plt.show()
# ============================================================
# 19. 累積資産
# ============================================================
plt.figure(figsize=(4, 2.5))
plt.plot(
    equity.index,
    equity,
)
plt.title(
    "MAGS・現物バスケット統計的裁定の累積資産"
)
plt.xlabel("Date")
plt.ylabel("Equity")
plt.grid()
plt.show()
# ============================================================
# 20. CSV保存
# ============================================================
result.to_csv(
    "mags_stock_basket_statistical_arbitrage.csv",
    encoding="utf-8-sig",
)

参考

Python3ではじめるシステムトレード【第2版】環境構築と売買戦略

「画像をクリックしていただくとpanrollingのホームページから書籍を購入していただけます。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?