galaxis-po/backend/app/services/strategy/value_momentum.py
zephyrdark 3f8ef7e108 feat: add multi-factor, quality, and value-momentum strategies
- BaseStrategy abstract class
- MultiFactorStrategy with weighted factors
- QualityStrategy with F-Score filtering
- ValueMomentumStrategy combining value and momentum

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 08:59:05 +09:00

99 lines
3.4 KiB
Python

"""
Value-Momentum strategy implementation.
"""
from datetime import date
from decimal import Decimal
from app.services.strategy.base import BaseStrategy
from app.schemas.strategy import StockFactor, StrategyResult, UniverseFilter
class ValueMomentumStrategy(BaseStrategy):
"""Value-Momentum combined strategy."""
strategy_name = "value_momentum"
def run(
self,
universe_filter: UniverseFilter,
top_n: int,
base_date: date = None,
value_weight: Decimal = Decimal("0.5"),
momentum_weight: Decimal = Decimal("0.5"),
) -> StrategyResult:
if base_date is None:
base_date = date.today()
# Get universe
stocks = self.get_universe(universe_filter)
tickers = [s.ticker for s in stocks]
stock_map = {s.ticker: s for s in stocks}
if not tickers:
return StrategyResult(
strategy_name=self.strategy_name,
base_date=base_date,
universe_count=0,
result_count=0,
stocks=[],
)
# Get data
valuations = self.factor_calc.get_valuations(tickers, base_date)
sectors = self.factor_calc.get_sectors(tickers)
value_scores = self.factor_calc.calculate_value_scores(valuations)
momentum = self.factor_calc.calculate_momentum(tickers, base_date)
# Normalize momentum
if momentum:
mom_values = list(momentum.values())
mom_mean = sum(mom_values) / len(mom_values)
mom_std = (sum((v - mom_mean) ** 2 for v in mom_values) / len(mom_values)) ** 0.5
if mom_std > 0:
momentum_scores = {
t: Decimal(str((float(v) - float(mom_mean)) / float(mom_std)))
for t, v in momentum.items()
}
else:
momentum_scores = {t: Decimal("0") for t in momentum}
else:
momentum_scores = {}
# Build results
results = []
for ticker in tickers:
stock = stock_map[ticker]
val = valuations.get(ticker)
v_score = value_scores.get(ticker, Decimal("0"))
m_score = momentum_scores.get(ticker, Decimal("0"))
total = v_score * value_weight + m_score * momentum_weight
results.append(StockFactor(
ticker=ticker,
name=stock.name,
market=stock.market,
sector_name=sectors.get(ticker),
market_cap=int(stock.market_cap / 100_000_000) if stock.market_cap else None,
close_price=Decimal(str(stock.close_price)) if stock.close_price else None,
per=Decimal(str(val.per)) if val and val.per else None,
pbr=Decimal(str(val.pbr)) if val and val.pbr else None,
dividend_yield=Decimal(str(val.dividend_yield)) if val and val.dividend_yield else None,
value_score=v_score,
momentum_score=m_score,
total_score=total,
))
results.sort(key=lambda x: x.total_score or Decimal("0"), reverse=True)
for i, r in enumerate(results[:top_n], 1):
r.rank = i
return StrategyResult(
strategy_name=self.strategy_name,
base_date=base_date,
universe_count=len(stocks),
result_count=min(top_n, len(results)),
stocks=results[:top_n],
)