feat(indicators): add validation and force_update to indicator calculations

This commit is contained in:
Giulio De Pasquale 2025-10-18 11:33:24 +01:00
parent 30ed373ac5
commit d87733b80e

View File

@ -52,6 +52,17 @@ class IndicatorService:
IndicatorsData instance ready to save to database, or None if insufficient data
or if any calculated values are NaN
"""
# Add indicator-specific validation
required_periods = {
"MACD": 26 + 9, # slowperiod + signalperiod
"ADX": 14 + 14, # Additional warmup needed
"RSI": 14 + 1, # Plus warmup
}
min_required = max(required_periods.values()) + 10 # Safety buffer
if lookback_days < min_required:
return None
# Fetch historical OHLCV data from database
start_date: datetime = target_date - timedelta(
days=lookback_days * 2
@ -60,8 +71,7 @@ class IndicatorService:
ticker=ticker, start_date=start_date, end_date=target_date
)
# Verify we have enough data (minimum 40 trading days for MACD + ADX)
if len(ohlcv_records) < 40:
if len(ohlcv_records) < min_required:
return None
# Convert to numpy arrays for TA-Lib
@ -89,7 +99,6 @@ class IndicatorService:
):
return None
# Build and return strongly-typed IndicatorsData
return IndicatorsData(
ticker=ticker,
date=target_date,
@ -130,26 +139,21 @@ class IndicatorService:
)
def calculate_and_save_indicators(
self, ticker: str, target_date: datetime
self, ticker: str, target_date: datetime, force_update: bool = False
) -> IndicatorsData | None:
"""
Calculate indicators and immediately save them to the database.
if not force_update:
existing = self._crud.get_indicators(ticker=ticker, date=target_date)
Args:
ticker: Stock ticker symbol
target_date: Date to calculate indicators for
if existing is not None:
return existing
Returns:
Saved IndicatorsData instance or None if calculation failed
"""
indicators: IndicatorsData | None = self.calculate_indicators_for_date(
indicators = self.calculate_indicators_for_date(
ticker=ticker, target_date=target_date
)
if indicators is None:
return None
# Upsert (insert or update if exists)
return self._crud.upsert_indicators(indicators)
def bulk_calculate_indicators(
@ -177,34 +181,30 @@ class IndicatorService:
return results
def bulk_calculate_and_save_indicators(
self, ticker: str, dates: List[datetime]
) -> int:
"""
Calculate and save indicators for multiple dates efficiently.
Uses upsert logic to handle existing records.
Args:
ticker: Stock ticker symbol
dates: List of dates to calculate indicators for
Returns:
Number of indicator records successfully saved/updated
"""
indicators_list: List[IndicatorsData] = self.bulk_calculate_indicators(
ticker=ticker, dates=dates
)
if not indicators_list:
self, ticker: str, dates: List[datetime], force_update: bool = False
) -> int:
# Batch check existing records
if not force_update:
existing_dates = self._crud.get_existing_indicator_dates(ticker, dates)
dates = [d for d in dates if d not in existing_dates]
if not dates:
return 0
start_date = min(dates) - timedelta(days=150)
end_date = max(dates)
all_ohlcv = self._crud.get_ohlcv_range(ticker, start_date, end_date)
results = []
for date in dates:
indicators = self._calculate_for_date_from_data(all_ohlcv, date)
# Use upsert for each indicator to avoid UNIQUE constraint violations
saved_count = 0
for indicators in indicators_list:
result = self._crud.upsert_indicators(indicators)
if result:
saved_count += 1
if indicators:
results.append(indicators)
# Batch insert
return self._crud.bulk_upsert_indicators(results)
return saved_count
# ========================================================================
# PRIVATE: Data Conversion