From d87733b80eca37404d30982c4b70c661030331c3 Mon Sep 17 00:00:00 2001 From: Giulio De Pasquale Date: Sat, 18 Oct 2025 11:33:24 +0100 Subject: [PATCH] feat(indicators): add validation and force_update to indicator calculations --- paperone/indicators.py | 78 +++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/paperone/indicators.py b/paperone/indicators.py index ab094fc..4bc91ec 100644 --- a/paperone/indicators.py +++ b/paperone/indicators.py @@ -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