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