- 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>
85 lines
2.8 KiB
Python
85 lines
2.8 KiB
Python
"""
|
|
Super Quality 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 QualityStrategy(BaseStrategy):
|
|
"""Super Quality strategy using F-Score and quality factors."""
|
|
|
|
strategy_name = "quality"
|
|
|
|
def run(
|
|
self,
|
|
universe_filter: UniverseFilter,
|
|
top_n: int,
|
|
base_date: date = None,
|
|
min_fscore: int = 7,
|
|
) -> 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)
|
|
fscores = self.factor_calc.calculate_fscore(tickers)
|
|
quality_scores = self.factor_calc.calculate_quality_scores(tickers, base_date)
|
|
|
|
# Filter by F-Score
|
|
qualified_tickers = [t for t in tickers if fscores.get(t, 0) >= min_fscore]
|
|
|
|
# Build results
|
|
results = []
|
|
for ticker in qualified_tickers:
|
|
stock = stock_map[ticker]
|
|
val = valuations.get(ticker)
|
|
q_score = quality_scores.get(ticker, Decimal("0"))
|
|
fscore = fscores.get(ticker, 0)
|
|
|
|
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,
|
|
quality_score=q_score,
|
|
total_score=q_score,
|
|
fscore=fscore,
|
|
))
|
|
|
|
# Sort by quality score
|
|
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],
|
|
)
|