aqr-factor-investing について
クオンツ投資の研究者や戦略開発者向けのファクター投資スキルです。学術研究と経済的根拠に基づく設計、分散、現実的な取引コストを、コード例とともに示します。
主な機能
- 01研究に基づくファクター選定
- 02複合ファクターとマルチファクター構築
- 03取引コストと流動性の考慮
- 04ファクター帰属分析と透明な報告
- 05期間や地域をまたぐ検証
活用シーン
- 01経済的根拠のあるファクター戦略の設計
- 02取引コスト控除後のリターン評価
- 03分散型マルチファクターポートフォリオの構築
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ copyleftdev/ sk1llz --skill aqr-factor-investingインストール後、Claude Code で/aqr-factor-investingを入力して読み込まれたか確認します。
詳細
AQR Capital Management スタイルガイド
概要
AQR(Applied Quantitative Research)は、Cliff Asness と Goldman Sachs のほかの研究者によって設立された、約1,000億ドルを運用するクオンツ投資会社です。学術的なファクター研究を実践的な投資に取り入れたことで知られ、透明性、厳格な方法論、クオンツ手法の民主化を重視しています。
基本理念
「金融における最良のアイデアは、ウォール街の直感ではなく、厳格な学術研究から生まれます。」
「ファクターが機能するのは、リスク、行動、または構造によるものです。投資する前に、どれに当たるのかを理解しましょう。」
「簡潔に説明できないなら、十分に理解できていません。」
AQR は、体系的なファクター(バリュー、モメンタム、クオリティなど)は、規律ある運用によって獲得できる、持続的なリターンの源泉だと考えています。戦略が機能するという事実だけでなく、なぜ機能するのかを理解することを重視しています。
設計原則
-
学術的基盤:査読済みの研究を出発点とします。
-
ファクター規律:経済的な根拠があるファクターに絞ります。
-
透明性:方法論を公開し、過ちを認めます。
-
分散投資:ファクター、地域、資産クラスにわたって分散します。
-
実装が重要:取引コストによって、理論上のリターンが失われることがあります。
ファクター戦略を構築する際
必ず行うこと
- 学術研究を戦略の根拠とします
- 経済的な根拠(リスク、行動、構造)を理解します
- 複数の期間と地域でテストします
- 現実的な取引コストを考慮します
- 分散のために複数のファクターを組み合わせます
- 投資適格となるようファクターを構築します(流動性、運用可能規模)
決して行わないこと
- データマイニングで発見されたファクターを追いかけます
- 実装の乖離(理論上のリターンと実際のリターン)を無視します
- ファクター・プレミアムが時間の経過とともに安定していると仮定します
- 単一のファクターや市場に集中します
- ファクターの混雑を忘れます
- 必要以上に取引します
優先すること
- 単一の指標より複合ファクター
- 純粋なファクター・エクスポージャーを得るにはロングオンリーよりロング・ショート
- 均等金額加重より均等リスク加重
- 一括取引より段階的なリバランス
- 取引コストを考慮した最適化
- ファクター・タイミングへの懐疑的な姿勢
コードパターン
ファクター構築
class FactorBuilder:
"""
AQR-style factor construction: robust, diversified, investment-grade.
"""
def __init__(self, data_provider):
self.data = data_provider
def build_value_factor(self,
universe: List[str],
date: date) -> pd.Series:
"""
Value factor: composite of multiple value metrics.
AQR uses book/price, earnings/price, forecast earnings/price, etc.
"""
metrics = {}
# Book to Price (classic Fama-French)
metrics['book_to_price'] = self.data.get_fundamentals(
universe, 'book_value', date
) / self.data.get_prices(universe, date)
# Earnings to Price
metrics['earnings_to_price'] = self.data.get_fundamentals(
universe, 'trailing_earnings', date
) / self.data.get_prices(universe, date)
# Forward Earnings to Price (analyst estimates)
metrics['forward_ep'] = self.data.get_fundamentals(
universe, 'forward_earnings', date
) / self.data.get_prices(universe, date)
# Cash Flow to Price
metrics['cf_to_price'] = self.data.get_fundamentals(
universe, 'operating_cf', date
) / self.data.get_prices(universe, date)
# Composite: z-score and average
composite = pd.DataFrame(metrics)
z_scores = composite.apply(lambda x: self.winsorize_and_zscore(x), axis=0)
return z_scores.mean(axis=1)
def build_momentum_factor(self,
universe: List[str],
date: date) -> pd.Series:
"""
Momentum: 12-month return, skipping most recent month.
Classic Jegadeesh-Titman with AQR refinements.
"""
# 12-1 momentum (skip last month to avoid reversal)
prices = self.data.get_price_history(universe, date, lookback_months=13)
# Return from t-12 to t-1
momentum_12_1 = prices.iloc[-22] / prices.iloc[0] - 1 # Skip last month
# AQR enhancement: also consider intermediate momentum
momentum_6_1 = prices.iloc[-22] / prices.iloc[-132] - 1
# Industry-adjusted (avoid sector bets)
industries = self.data.get_industries(universe)
mom_adj = momentum_12_1.groupby(industries).transform(
lambda x: x - x.mean()
)
return self.winsorize_and_zscore(mom_adj)
def build_quality_factor(self,
universe: List[str],
date: date) -> pd.Series:
"""
Quality: profitability, stability, and financial health.
Based on AQR's "Quality Minus Junk" research.
"""
profitability = self.calculate_profitability(universe, date)
growth = self.calculate_growth_stability(universe, date)
safety = self.calculate_safety(universe, date)
payout = self.calculate_payout(universe, date)
# Composite quality score
quality = pd.DataFrame({
'profitability': self.winsorize_and_zscore(profitability),
'growth': self.winsorize_and_zscore(growth),
'safety': self.winsorize_and_zscore(safety),
'payout': self.winsorize_and_zscore(payout)
})
return quality.mean(axis=1)
def calculate_profitability(self, universe, date):
"""Gross profits / assets, ROE, ROA, etc."""
gp = self.data.get_fundamentals(universe, 'gross_profit', date)
assets = self.data.get_fundamentals(universe, 'total_assets', date)
return gp / assets
def calculate_safety(self, universe, date):
"""Low leverage, low volatility, low beta."""
leverage = self.data.get_fundamentals(universe, 'debt_to_equity', date)
volatility = self.data.get_volatility(universe, date, lookback_days=252)
# Invert so higher is better
return -(leverage.rank() + volatility.rank()) / 2
def winsorize_and_zscore(self, series: pd.Series, clip_std: float = 3.0):
"""Winsorize outliers and standardize."""
z = (series - series.mean()) / series.std()
z = z.clip(-clip_std, clip_std)
return (z - z.mean()) / z.std()
マルチファクター・ポートフォリオの構築
class FactorPortfolio:
"""
AQR's portfolio construction: factor exposure with risk management.
"""
def __init__(self, factors: Dict[str, FactorBuilder],
risk_model: RiskModel,
transaction_cost_model: TCostModel):
self.factors = factors
self.risk = risk_model
self.tcost = transaction_cost_model
def construct_portfolio(self,
universe: List[str],
date: date,
factor_weights: Dict[str, float],
risk_target: float = 0.10) -> pd.Series:
"""
Build a portfolio with target factor exposures.
"""
# Calculate factor scores
factor_scores = {}
for name, builder in self.factors.items():
factor_scores[name] = builder.build(universe, date)
# Combine factors with weights
combined_score = sum(
factor_scores[name] * weight
for name, weight in factor_weights.items()
)
# Convert scores to weights (long-short)
raw_weights = self.scores_to_weights(combined_score)
# Scale to target risk
portfolio_vol = self.risk.estimate_volatility(raw_weights)
scaled_weights = raw_weights * (risk_target / portfolio_vol)
return scaled_weights
def scores_to_weights(self, scores: pd.Series) -> pd.Series:
"""
Convert z-scores to portfolio weights.
AQR approach: proportional to score, with constraints.
"""
# Long top tercile, short bottom tercile
n = len(scores)
tercile = n // 3
sorted_idx = scores.sort_values().index
weights = pd.Series(0.0, index=scores.index)
weights[sorted_idx[:tercile]] = -1.0 / tercile # Short bottom
weights[sorted_idx[-tercile:]] = 1.0 / tercile # Long top
return weights
def calculate_turnover_cost(self,
current: pd.Series,
target: pd.Series,
date: date) -> float:
"""
Estimate transaction costs from rebalancing.
"""
trades = (target - current).abs()
costs = self.tcost.estimate(trades, date)
return costs.sum()
def optimize_with_turnover(self,
current: pd.Series,
target: pd.Series,
max_turnover_cost: float) -> pd.Series:
"""
Trade toward target, but respect turnover budget.
"""
trades = target - current
# If unconstrained cost is acceptable, trade fully
full_cost = self.calculate_turnover_cost(current, target, date)
if full_cost <= max_turnover_cost:
return target
# Otherwise, trade partially (proportionally)
trade_fraction = max_turnover_cost / full_cost
return current + trades * trade_fraction
ファクターのアトリビューションとレポート
class FactorAttribution:
"""
AQR-style transparent performance attribution.
Understand exactly where returns came from.
"""
def __init__(self, factor_returns: pd.DataFrame):
self.factor_returns = factor_returns
def attribute_returns(self,
portfolio_returns: pd.Series,
factor_exposures: pd.DataFrame) -> AttributionResult:
"""
Decompose portfolio returns into factor contributions.
R_p = Σ(β_i * F_i) + α + ε
"""
# Align data
common_dates = portfolio_returns.index.intersection(
self.factor_returns.index
)
port_ret = portfolio_returns.loc[common_dates]
fact_ret = self.factor_returns.loc[common_dates]
exposures = factor_exposures.loc[common_dates]
# Calculate factor contributions
contributions = {}
total_factor_return = 0
for factor in fact_ret.columns:
factor_contribution = (exposures[factor] * fact_ret[factor]).sum()
contributions[factor] = {
'avg_exposure': exposures[factor].mean(),
'factor_return': fact_ret[factor].sum(),
'contribution': factor_contribution,
'contribution_pct': factor_contribution / port_ret.sum() * 100
}
total_factor_return += factor_contribution
# Alpha is unexplained return
alpha = port_ret.sum() - total_factor_return
return AttributionResult(
total_return=port_ret.sum(),
factor_contributions=contributions,
alpha=alpha,
r_squared=self.calculate_r_squared(port_ret, fact_ret, exposures)
)
def factor_performance_report(self,
start_date: date,
end_date: date) -> pd.DataFrame:
"""
Generate factor performance summary.
AQR publishes these regularly for transparency.
"""
returns = self.factor_returns.loc[start_date:end_date]
report = pd.DataFrame({
'Total Return': returns.sum(),
'Annualized Return': returns.mean() * 252,
'Volatility': returns.std() * np.sqrt(252),
'Sharpe Ratio': returns.mean() / returns.std() * np.sqrt(252),
'Max Drawdown': self.calculate_max_drawdown(returns),
'Hit Rate': (returns > 0).mean()
})
return report
現実的な摩擦を考慮したバックテスト
class RealisticBacktest:
"""
AQR emphasizes the gap between paper and real returns.
Model all frictions realistically.
"""
def __init__(self,
tcost_model: TransactionCostModel,
borrow_cost_model: BorrowCostModel,
market_impact_model: MarketImpactModel):
self.tcost = tcost_model
self.borrow = borrow_cost_model
self.impact = market_impact_model
def run_backtest(self,
strategy: Strategy,
start_date: date,
end_date: date,
initial_capital: float = 1e8) -> BacktestResult:
"""
Backtest with realistic transaction costs and frictions.
"""
capital = initial_capital
positions = pd.Series(dtype=float)
results = []
for date in trading_days(start_date, end_date):
# Generate target portfolio
target = strategy.generate_positions(date, capital)
# Calculate trading costs
trades = target - positions
trading_cost = self.tcost.estimate(trades, date)
market_impact = self.impact.estimate(trades, date)
# Borrow costs for short positions
short_positions = positions[positions < 0]
borrow_cost = self.borrow.estimate(short_positions, date)
# Execute trades (adjust for costs)
capital -= trading_cost + market_impact
positions = target
# Calculate return
price_returns = self.get_returns(positions.index, date)
gross_pnl = (positions * price_returns).sum()
net_pnl = gross_pnl - trading_cost - market_impact - borrow_cost
capital += net_pnl
results.append({
'date': date,
'gross_pnl': gross_pnl,
'trading_cost': trading_cost,
'market_impact': market_impact,
'borrow_cost': borrow_cost,
'net_pnl': net_pnl,
'capital': capital,
'turnover': trades.abs().sum() / capital
})
return self.analyze_results(pd.DataFrame(results))
def analyze_results(self, results: pd.DataFrame) -> BacktestResult:
"""Compute performance metrics with cost breakdown."""
gross_returns = results['gross_pnl'] / results['capital'].shift(1)
net_returns = results['net_pnl'] / results['capital'].shift(1)
return BacktestResult(
gross_sharpe=gross_returns.mean() / gross_returns.std() * np.sqrt(252),
net_sharpe=net_returns.mean() / net_returns.std() * np.sqrt(252),
implementation_drag=(gross_returns.sum() - net_returns.sum()) / len(results) * 252,
avg_turnover=results['turnover'].mean(),
total_trading_costs=results['trading_cost'].sum(),
total_impact_costs=results['market_impact'].sum(),
total_borrow_costs=results['borrow_cost'].sum()
)
思考モデル
AQR はファクター投資にあたり、次の点を問いかけます。
- 学術的なエビデンスはあるか? マーケティングではなく、査読済みの研究
- 経済的な説明は何か? リスク・プレミアム、行動バイアス、または構造的要因か?
- 取引コストを差し引いても成り立つか? 理論上のリターン ≠ 実際のリターン
- 混雑しているか? ファクターの人気がリターンを低下させます
- 大規模に実装できるか? 流動性と運用可能規模の制約
AQR ならではの取り組み
- 単一の指標より複合ファクター
- 学術研究に見合う質の高い調査プロセス
- 透明性の高い方法論
- 現実的な取引コストのモデリング
- 複数の資産クラスにわたる分散投資
- ファクター・タイミングへの懐疑的な姿勢
- 純粋なファクター・エクスポージャーを得るためのロング・ショート
- ベンチマーク用に公開されたファクター・リターン
コメント
Markdown 対応