Repository: Pavel14701/trade_project_bot
Pull request #18: Add multiple technical indicators
Author: login: Pavel14701
Created: 2025-05-11T08:00:40Z | Merged: 2025-05-11T08:45:28Z

## Краткое описание от Sourcery

Внедрены несколько новых технических индикаторов (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) и выполнена реструктуризация существующей реализации RSI Clouds для улучшения расширяемости и ясности. Обновлены определения типов и сущности конфигурации для поддержки новых индикаторов. Скорректированы импорты и ссылки для учета переименования модулей и новых модулей индикаторов. Расширены зависимости проекта для поддержки новых аналитических функций.

Новые функции:
- Добавлена реализация индикатора AVSL (Adaptive Volume-Weighted Support Level).
- Добавлена реализация индикатора SCRSI (SmoothCicle RSI).
- Добавлена реализация индикатора Stochastic RSI.
- Добавлена реализация индикатора Acceleration Bands.
- Добавлена реализация индикатора тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием реализации ZigZag peaks.

Улучшения:
- Реструктуризация индикатора RSI Clouds для улучшения ясности, расширяемости и английской документации.
- Обновлены PriceDataFrame и связанные определения типов для обеспечения согласованности и соответствия требованиям новых индикаторов.
- Расширены dataclass конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости (matplotlib, numba, scipy, yfinance) для поддержки вычислений индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Краткое описание от Sourcery

Внедрены несколько новых технических индикаторов (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) и реструктурирована реализация RSI Clouds для улучшения расширяемости и ясности. Обновлены определения типов и сущности конфигурации для поддержки новых индикаторов, а также скорректированы импорты и ссылки для учета переименования модулей и новых модулей. Расширены зависимости проекта для обеспечения расширенных аналитических вычислений.

Новые функции:
- Добавлен индикатор AVSL (Adaptive Volume-Weighted Support Level).
- Добавлен индикатор SCRSI (SmoothCicle RSI).
- Добавлен индикатор Stochastic RSI.
- Добавлен индикатор Acceleration Bands.
- Добавлен индикатор тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием пиков ZigZag.

Улучшения:
- Реструктурирован индикатор RSI Clouds для улучшения расширяемости и ясности.
- Обновлены PriceDataFrame и связанные определения типов для обеспечения согласованности и совместимости с новыми индикаторами.
- Расширены классы данных конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости (matplotlib, numba, scipy, yfinance) для поддержки вычислений индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Краткое описание от Sourcery

Внедрение нескольких новых технических индикаторов и реструктуризация существующей реализации RSI Clouds для повышения расширяемости и ясности. Обновление определений типов и сущностей конфигурации для поддержки новых индикаторов, а также корректировка импортов и ссылок для учета переименования и добавления модулей. Расширение зависимостей проекта для обеспечения новых аналитических функций.

Новые функции:
- Добавлен индикатор AVSL (Adaptive Volume-Weighted Support Level).
- Добавлен индикатор SCRSI (SmoothCicle RSI).
- Добавлен индикатор Stochastic RSI.
- Добавлен индикатор Acceleration Bands.
- Добавлен индикатор тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием пиков ZigZag.

Улучшения:
- Реструктуризация индикатора RSI Clouds для большей ясности и расширяемости.
- Обновление PriceDataFrame и связанных определений типов для обеспечения согласованности с новыми индикаторами.
- Расширение dataclass конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости: matplotlib, numba, scipy, yfinance для расчетов индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Обзор от Sourcery

Внедрены несколько новых технических индикаторов и реструктурирована существующая реализация RSI Clouds для улучшения расширяемости и ясности. Обновлены определения типов и сущности конфигурации для поддержки новых индикаторов, а также скорректированы импорты и ссылки для переименования модулей и новых модулей. Расширены зависимости проекта для обеспечения расширенных аналитических вычислений.

Новые функции:
- Добавлен индикатор AVSL (Adaptive Volume-Weighted Support Level).
- Добавлен индикатор SCRSI (SmoothCicle RSI).
- Добавлен индикатор Stochastic RSI.
- Добавлен индикатор Acceleration Bands.
- Добавлен индикатор тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием пиков ZigZag.

Улучшения:
- Реструктурирован индикатор RSI Clouds для большей ясности и расширяемости.
- Обновлены PriceDataFrame и связанные определения типов для согласованности с новыми индикаторами.
- Расширены dataclass конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости: matplotlib, numba, scipy, yfinance для вычислений индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Обзор от Sourcery

Внедрены несколько новых технических индикаторов (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) и реструктурирована реализация RSI Clouds для улучшения расширяемости и ясности. Обновлены определения типов и сущности конфигурации для поддержки новых индикаторов, а также скорректированы импорты и ссылки для учета переименования модулей и новых модулей. Расширены зависимости проекта для обеспечения расширенных аналитических вычислений.

Новые функции:
- Добавлен индикатор AVSL (Adaptive Volume-Weighted Support Level).
- Добавлен индикатор SCRSI (SmoothCicle RSI).
- Добавлен индикатор Stochastic RSI.
- Добавлен индикатор Acceleration Bands.
- Добавлен индикатор тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием пиков ZigZag.

Улучшения:
- Реструктурирован индикатор RSI Clouds для большей ясности и расширяемости.
- Обновлены PriceDataFrame и связанные определения типов для согласованности с новыми индикаторами.
- Расширены классы данных конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости: matplotlib, numba, scipy, yfinance для вычислений индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Обзор от Sourcery

Внедрены несколько новых технических индикаторов (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) и реструктурирована реализация RSI Clouds для улучшения расширяемости и ясности. Обновлены определения типов и сущности конфигурации для поддержки новых индикаторов, а также скорректированы импорты и ссылки для учета переименования модулей и новых модулей. Расширены зависимости проекта для обеспечения расширенных аналитических вычислений.

Новые функции:
- Добавлен индикатор AVSL (Adaptive Volume-Weighted Support Level).
- Добавлен индикатор SCRSI (SmoothCicle RSI).
- Добавлен индикатор Stochastic RSI.
- Добавлен индикатор Acceleration Bands.
- Добавлен индикатор тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием пиков ZigZag.

Улучшения:
- Реструктурирован индикатор RSI Clouds для улучшения ясности и расширяемости.
- Обновлены PriceDataFrame и связанные определения типов для согласованности с новыми индикаторами.
- Расширены классы данных конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости: matplotlib, numba, scipy, yfinance для вычислений индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Introduce multiple new technical indicators (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) and restructure the RSI Clouds implementation for improved extensibility and clarity. Update type definitions and configuration entities to support the new indicators, and adjust imports and references to accommodate module renaming and new modules. Expand project dependencies to enable advanced analytical computations.

New Features:
- Add AVSL (Adaptive Volume-Weighted Support Level) indicator.
- Add SCRSI (SmoothCicle RSI) indicator.
- Add Stochastic RSI indicator.
- Add Acceleration Bands indicator.
- Add ADX (Average Directional Index) trend indicator.
- Add Order Block Detector using ZigZag peaks.

Enhancements:
- Restructure RSI Clouds indicator for improved clarity and extensibility.
- Update PriceDataFrame and related type definitions for consistency with new indicators.
- Expand configuration dataclasses to support new indicators.

Build:
- Add new dependencies: matplotlib, numba, scipy, yfinance for indicator calculations and analytics.

</details>

Новые возможности:
- Добавлен индикатор AVSL (Adaptive Volume-Weighted Support Level).
- Добавлен индикатор SCRSI (SmoothCicle RSI).
- Добавлен индикатор Stochastic RSI.
- Добавлен индикатор Acceleration Bands.
- Добавлен индикатор тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием пиков ZigZag.

Улучшения:
- Реструктурирован индикатор RSI Clouds для улучшения ясности и расширяемости.
- Обновлены PriceDataFrame и связанные определения типов для обеспечения согласованности с новыми индикаторами.
- Расширены классы данных конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости: matplotlib, numba, scipy, yfinance для вычислений индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Обзор от Sourcery

Внедрены несколько новых технических индикаторов (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) и реструктурирована реализация RSI Clouds для улучшения расширяемости и ясности. Обновлены определения типов и сущности конфигурации для поддержки новых индикаторов, а также скорректированы импорты и ссылки для учета переименования модулей и новых модулей. Расширены зависимости проекта для обеспечения расширенных аналитических вычислений.

Новые функции:
- Добавлен индикатор AVSL (Adaptive Volume-Weighted Support Level).
- Добавлен индикатор SCRSI (SmoothCicle RSI).
- Добавлен индикатор Stochastic RSI.
- Добавлен индикатор Acceleration Bands.
- Добавлен индикатор тренда ADX (Average Directional Index).
- Добавлен Order Block Detector с использованием пиков ZigZag.

Улучшения:
- Реструктурирован индикатор RSI Clouds для улучшения ясности и расширяемости.
- Обновлены PriceDataFrame и связанные определения типов для согласованности с новыми индикаторами.
- Расширены классы данных конфигурации для поддержки новых индикаторов.

Сборка:
- Добавлены новые зависимости: matplotlib, numba, scipy, yfinance для вычислений индикаторов и аналитики.

<details>
<summary>Original summary in English</summary>

## Summary by Sourcery

Introduce multiple new technical indicators (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) and restructure the RSI Clouds implementation for improved extensibility and clarity. Update type definitions and configuration entities to support the new indicators, and adjust imports and references to accommodate module renaming and new modules. Expand project dependencies to enable advanced analytical computations.

New Features:
- Add AVSL (Adaptive Volume-Weighted Support Level) indicator.
- Add SCRSI (SmoothCicle RSI) indicator.
- Add Stochastic RSI indicator.
- Add Acceleration Bands indicator.
- Add ADX (Average Directional Index) trend indicator.
- Add Order Block Detector using ZigZag peaks.

Enhancements:
- Restructure RSI Clouds indicator for improved clarity and extensibility.
- Update PriceDataFrame and related type definitions for consistency with new indicators.
- Expand configuration dataclasses to support new indicators.

Build:
- Add new dependencies: matplotlib, numba, scipy, yfinance for indicator calculations and analytics.

</details>

</details>

</details>

</details>

</details>

</details>

</details>


Diff:
diff --git a/.gitignore b/.gitignore
index 022797c..f5b8b57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,8 @@ __pycache__
 .ruff_cache
 legacy
 secrets.txt
-backups
\ No newline at end of file
+backups
+zigzag_spy.png
+order_block_develop.py
+scrsi_develop.py
+scrsi_spy.png
\ No newline at end of file
diff --git a/main_app/src/application/interfaces.py b/main_app/src/application/interfaces.py
index 116a520..7f1507b 100644
--- a/main_app/src/application/interfaces.py
+++ b/main_app/src/application/interfaces.py
@@ -11,7 +11,7 @@
     UserSignupDM,
     WebSocketDM,
 )
-from main_app.src.infrastructure.types import (
+from main_app.src.infrastructure._types import (
     ExceptionType,
     GuestSessionData,
     GuestSessionID,
diff --git a/main_app/src/infrastructure/types.py b/main_app/src/infrastructure/_types.py
similarity index 100%
rename from main_app/src/infrastructure/types.py
rename to main_app/src/infrastructure/_types.py
diff --git a/main_app/src/infrastructure/middlewares.py b/main_app/src/infrastructure/middlewares.py
index 033dac4..3bfbc4a 100644
--- a/main_app/src/infrastructure/middlewares.py
+++ b/main_app/src/infrastructure/middlewares.py
@@ -8,7 +8,7 @@
     GuestSessionBackend,
     RedisSessionBackend,
 )
-from main_app.src.infrastructure.types import RequestResponseEndpoint
+from main_app.src.infrastructure._types import RequestResponseEndpoint
 
 
 class SessionMiddleware(BaseHTTPMiddleware):
diff --git a/main_app/src/infrastructure/models.py b/main_app/src/infrastructure/models.py
index 3a267e6..ff62cd5 100644
--- a/main_app/src/infrastructure/models.py
+++ b/main_app/src/infrastructure/models.py
@@ -10,7 +10,7 @@
     relationship,
 )
 
-from main_app.src.infrastructure.types import DomainModel
+from main_app.src.infrastructure._types import DomainModel
 
 
 class Base(DeclarativeBase):
diff --git a/pyproject.toml b/pyproject.toml
index 43806e9..ec5134b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,17 +14,21 @@ dependencies = [
     "fastapi>=0.115.12",
     "faststream>=0.5.39",
     "httpx>=0.28.1",
+    "matplotlib>=3.10.3",
     "mypy>=1.15.0",
+    "numba>=0.61.2",
     "pandas>=2.2.3",
     "pandas-stubs>=2.2.3.250308",
     "pandas-ta>=0.3.14b0",
     "psycopg>=3.2.6",
     "redis>=5.2.1",
     "ruff>=0.11.7",
+    "scipy>=1.15.3",
     "sqlalchemy>=2.0.40",
     "ta-lib>=0.6.3",
     "uvicorn>=0.34.2",
     "websockets>=15.0.1",
+    "yfinance>=0.2.59",
 ]
 
 [tool.setuptools]
diff --git a/strategies/src/application/interfaces.py b/strategies/src/application/interfaces.py
index dba9be3..e1263c8 100644
--- a/strategies/src/application/interfaces.py
+++ b/strategies/src/application/interfaces.py
@@ -7,7 +7,7 @@
     InsertManyRecordsDM,
     InsertRecordDM,
 )
-from strategies.src.infrastructure.types import PriceDataFrame
+from strategies.src.infrastructure._types import PriceDataFrame
 
 
 class IDataQuery(Protocol):
diff --git a/strategies/src/domain/__init__.py b/strategies/src/domain/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/strategies/src/domain/entities.py b/strategies/src/domain/entities.py
index 77aaf3a..760ebb3 100644
--- a/strategies/src/domain/entities.py
+++ b/strategies/src/domain/entities.py
@@ -1,4 +1,5 @@
 from dataclasses import dataclass
+from typing import Any
 
 
 @dataclass(slots=True, frozen=True)
@@ -57,3 +58,59 @@ class RsiCloudsConfigDM:
     drift: int | None = None
     offset: int | None = None
     talib: bool = True
+
+
+@dataclass(slots=True, frozen=True)
+class AdxConfigDM:
+    length: int | None = None
+    lensig: int | None = None
+    mamode: str | None = None 
+    scalar: float | int | None = None 
+    drift: int | None = None
+    offset: int | None = None
+
+
+@dataclass(slots=True, frozen=True)
+class AvslConfigDM:
+    length_fast: int
+    length_slow: int
+    stand_div: float
+
+
+@dataclass(slots=True, frozen=True)
+class StochRsiConfigDM:
+    timeperiod: int
+    fastk_period: int 
+    fastd_period: int
+    #talib.MA_Type
+    fastd_matype: int
+
+
+@dataclass(slots=True, frozen=True)
+class AcceletrationBandsDM:
+    length: int
+    drift: int
+    offset: int
+    mamode: str | None = None
+
+
+@dataclass(slots=True, frozen=True)
+class OrderBlockDetectorDM:
+    peak_prominance: float = 0.02 
+    valley_prominance: float = -0.02 
+    width: int = 1
+    distance: int = 3 
+    height:  Any | None = None
+    threshold: Any | None = None
+    prominence: Any | None = None
+    wlen: Any | None = None
+    rel_height: float = 0.5
+    plateu_size: Any | None  = None
+
+
+@dataclass(slots=True, frozen=True)
+class ScrsiConfigDM:
+    domcycle: int = 20  # Основной цикл CRSI
+    vibration: int = 20  # Колебания
+    leveling: float =  10.0  # Уровень сглаживания
+
diff --git a/strategies/src/infrastructure/types.py b/strategies/src/infrastructure/_types.py
similarity index 75%
rename from strategies/src/infrastructure/types.py
rename to strategies/src/infrastructure/_types.py
index 2625a40..294e9ee 100644
--- a/strategies/src/infrastructure/types.py
+++ b/strategies/src/infrastructure/_types.py
@@ -1,7 +1,7 @@
-from pandas import DataFrame, Index, Series, to_datetime
+from typing import Union
+import pandas as pd
 
-
-class PriceDataFrame(DataFrame):
+class PriceDataFrame(pd.DataFrame):
     """
     Кастомный DataFrame с автоматической обработкой
     типов QuestDB и доступом к колонкам через атрибуты.
@@ -16,7 +16,7 @@ def _convert_types(self):
         Приводим типы данных в соответствие
         с таблицей QuestDB.
         """
-        self["date"] = to_datetime(self["date"])
+        self["date"] = pd.to_datetime(self["date"])
         self["open_price"] = self["open_price"].astype(float)
         self["close_price"] = self["close_price"].astype(float)
         self["high_price"] = self["high_price"].astype(float)
@@ -25,29 +25,29 @@ def _convert_types(self):
         self["turnover"] = self["turnover"].astype(float)
 
     @property
-    def date(self) -> Index:
+    def date(self) -> pd.Index:
         return self.index
 
     @property
-    def open_price(self) -> Series:
+    def open_price(self) -> pd.Series:
         return self["open_price"]
 
     @property
-    def close_price(self) -> Series:
+    def close_prices(self) -> pd.Series:
         return self["close_price"]
 
     @property
-    def high_price(self) -> Series:
+    def high_prices(self) -> pd.Series:
         return self["high_price"]
 
     @property
-    def low_price(self) -> Series:
+    def low_prices(self) -> pd.Series:
         return self["low_price"]
 
     @property
-    def volume(self) -> Series:
+    def volumes(self) -> pd.Series:
         return self["volume"]
 
     @property
-    def turnover(self) -> Series:
+    def turnover(self) -> pd.Series:
         return self["turnover"]
diff --git a/strategies/src/infrastructure/database.py b/strategies/src/infrastructure/database.py
index 5ad16c2..e2ce680 100644
--- a/strategies/src/infrastructure/database.py
+++ b/strategies/src/infrastructure/database.py
@@ -11,7 +11,7 @@
     InsertManyRecordsDM,
     InsertRecordDM,
 )
-from strategies.src.infrastructure.types import PriceDataFrame
+from strategies.src.infrastructure._types import PriceDataFrame
 
 
 class DataQueryRepo(IDataQuery):
diff --git a/strategies/src/infrastructure/indicators/acceleration_bands.py b/strategies/src/infrastructure/indicators/acceleration_bands.py
new file mode 100644
index 0000000..c02ac25
--- /dev/null
+++ b/strategies/src/infrastructure/indicators/acceleration_bands.py
@@ -0,0 +1,124 @@
+import pandas as pd
+from pandas_ta import accbands
+import numpy as np
+
+from strategies.src.domain.entities import AcceletrationBandsDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class AccelerationBands:
+    """
+    Acceleration Bands Indicator Implementation.
+
+    Acceleration Bands (ACCB) were developed by Mark Helweg and 
+    are designed to indicate volatility and acceleration in 
+    price movements. These bands consist of an **upper band**, 
+    **lower band**, and **centerline** derived from a 
+    moving average.
+
+    Formula:
+    1. Compute a Moving Average (`mamode`) over the defined `length`.
+    2. Calculate the **upper acceleration band**: MA * (1 + percentage factor).
+    3. Calculate the **lower acceleration band**: MA * (1 - percentage factor).
+    4. Identify **buy and sell signals** when prices interact with these bands.
+    Usage:
+    - Helps detect periods of **increasing or decreasing price acceleration**.
+    - Can be used to identify **potential breakouts** when price breaches the bands.
+    """
+
+    def calc_accbands(
+        self, 
+        data: PriceDataFrame, 
+        config: AcceletrationBandsDM
+    ) -> pd.DataFrame:
+        """
+        Calculates Acceleration Bands (ACCB) values.
+        Args:
+            data (PriceDataFrame): Price dataset containing 
+              high, low, and close prices.
+            config (AcceletrationBandsDM): Configuration for 
+              Acceleration Bands (length, smoothing 
+              method, drift, offset).
+        Returns:
+            pd.DataFrame: A DataFrame containing upper, 
+              lower, and center acceleration bands.
+        """
+        # Compute Acceleration Bands using pandas-ta
+        if acc_bands := accbands(
+            high=data.high_prices,
+            low=data.low_prices,
+            close=data.close_prices,
+            length=config.length,
+            drift=config.drift,
+            mamode=config.mamode,
+            offset=config.offset,
+        ):
+            # Align index with input data
+            acc_bands.index = data.index  
+            # Preserve close prices for signal generation
+            acc_bands['close_prices'] = data.close_prices  
+            return acc_bands
+        # Raise an error if calculation fails
+        raise ValueError("No data in DataFrame")
+
+    def generate_signals(self, data: pd.DataFrame) -> pd.DataFrame:
+        """
+        Generates trading signals based on Acceleration 
+        Bands interaction.
+        Signals:
+        - **Buy:** Price crosses **above** the lower band.
+        - **Sell:** Price crosses **below** the upper band.
+        Args:
+            data (pd.DataFrame): DataFrame containing 
+            Acceleration Bands and price data.
+        Returns:
+            pd.DataFrame: A DataFrame containing buy/sell signals.
+        """
+        # Extract bands
+        upper_band = data['ACCbands_upper'].values
+        lower_band = data['ACCbands_lower'].values
+        # Buy signal: Price crosses above lower band
+        buy_signal = (
+            data["close_prices"] < upper_band
+        ) & (
+            np.roll(data["close_prices"], 1) > upper_band
+        )
+        # Sell signal: Price crosses below upper band
+        sell_signal = (
+            data["close_prices"] > lower_band
+        ) & (
+            np.roll(data["close_prices"], 1) < lower_band
+        )
+        # Create a DataFrame to store signals
+        signals = pd.DataFrame(index=data.index)
+        signals['buy_signals'] = buy_signal.astype(int)
+        signals['sell_signals'] = sell_signal.astype(int)
+        return signals
+
+    def check_last_signal(
+        self, 
+        data: PriceDataFrame, 
+        config: AcceletrationBandsDM
+    ) -> str | None:
+        """
+        Determines the last trading signal based on Acceleration Bands.
+        - If the last bar has a buy signal → returns "long".
+        - If the last bar has a sell signal → returns "short".
+        - If no signal is detected → returns None.
+        Args:
+            data (PriceDataFrame): Market data with price information.
+            config (AcceletrationBandsDM): Configuration for Acceleration Bands.
+        Returns:
+            str | None: "long" if buy signal, "short" if sell signal, 
+            or None if no signal is present.
+        """
+        #  Compute Acceleration Bands values
+        acc_bands_df = self.calc_accbands(data, config)
+        # Generate buy/sell signals
+        signals_df = self.generate_signals(acc_bands_df)
+        # Determine the last signal based on recent price action
+        if signals_df["buy_signals"].iloc[-1] == 1:
+            return "long"
+        elif signals_df["sell_signals"].iloc[-1] == 1:
+            return "short"        
+        return None
diff --git a/strategies/src/infrastructure/indicators/adx.py b/strategies/src/infrastructure/indicators/adx.py
new file mode 100644
index 0000000..415b978
--- /dev/null
+++ b/strategies/src/infrastructure/indicators/adx.py
@@ -0,0 +1,95 @@
+import pandas_ta as ta
+import pandas as pd
+
+from strategies.src.domain.entities import AdxConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class ADXTrend:
+    """
+    Average Directional Index (ADX) Trend Indicator Implementation.
+    The ADX measures the strength of a trend and is 
+    composed of three components:
+    - **ADX**: Indicates the trend strength 
+      (values above 25 suggest a strong trend).
+    - **DMP (+DI)**: Positive directional movement 
+      (upward trend indicator).
+    - **DMN (-DI)**: Negative directional movement 
+      (downward trend indicator).
+    Formula:
+    1. Compute **True Range (TR)**, which reflects market volatility.
+    2. Calculate the **Directional Movement (+DM / -DM)** based on 
+      high/low price action.
+    3. Apply **Moving Averages** to smooth +DM, -DM values.
+    4. Compute **ADX** as an exponentially smoothed average 
+      of the Directional Index.
+    Usage:
+    - Helps traders determine if a trend is strong 
+      enough to justify entering a trade.
+    - Can be combined with other trend indicators 
+      (e.g., moving averages) for confirmation.
+    """
+
+    def calculate_adx(
+        self, 
+        data: PriceDataFrame, 
+        config: AdxConfigDM
+    ) -> pd.DataFrame:
+        """
+        Computes the ADX, DMP (+DI), and DMN (-DI) trend indicators.
+        Args:
+            data (PriceDataFrame): Price dataset containing 
+              high, low, and close prices.
+            config (AdxConfigDM): Configuration for ADX 
+              (length, smoothing factors, drift, offset).
+        Returns:
+            pd.DataFrame: A DataFrame containing ADX, DMP, and DMN trend values.
+        Raises:
+            ValueError: If the ADX calculation returns missing 
+              values for any required component.
+        """
+        # Compute ADX and its components using pandas-ta
+        adx_ind = ta.adx(
+            high=data.high_prices,
+            low=data.low_prices,
+            close=data.close_prices,
+            length=config.length,
+            # Signal length for ADX smoothing
+            lensig=config.lensig,
+            # Scaling factor for values
+            scalar=config.scalar,  
+            # Moving average mode (e.g., EMA, SMA)
+            mamode=config.mamode,  
+            drift=config.drift,
+            offset=config.offset
+        )
+        #  Validate if all required components exist
+        if missing_indicators := [
+            indicator
+            for indicator in ["ADX", "DMP", "DMN"]
+            if adx_ind.get(f"{indicator}_{config.length}") is None
+        ]:
+            raise ValueError(
+                f"""Error: Missing required ADX components: 
+                {', '.join(missing_indicators)}."""
+            )
+        # Return structured DataFrame
+        return pd.DataFrame(
+            {
+                "adx": adx_ind[f"ADX_{config.lensig}"],
+                "dmp": adx_ind[f"DMP_{config.length}"],
+                "dmn": adx_ind[f"DMN_{config.length}"],
+            },
+            index=data.index
+        )
+
+    def get_signal(self, adx_trigger: int, data: pd.DataFrame) -> bool:
+        """
+        Determines whether the trend strength exceeds a defined threshold.
+        Args:
+            adx_trigger (int): Minimum ADX value required to confirm a trend.
+            data (pd.DataFrame): DataFrame containing ADX values.
+        Returns:
+            bool: True if ADX exceeds the threshold, False otherwise.
+        """
+        return int(data["adx"].iloc[-1]) >= adx_trigger
diff --git a/strategies/src/infrastructure/indicators/avsl.py b/strategies/src/infrastructure/indicators/avsl.py
new file mode 100644
index 0000000..89e7fae
--- /dev/null
+++ b/strategies/src/infrastructure/indicators/avsl.py
@@ -0,0 +1,268 @@
+import pandas as pd
+import numpy as np
+from numba import njit
+from numpy.typing import NDArray
+import pandas_ta as ta
+
+from strategies.src.domain.entities import AvslConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class AVSL:
+    """
+    Adaptive Volume-Weighted Support Level (
+      AVSL) Indicator Implementation.
+
+    The AVSL indicator calculates 
+      **dynamic support and resistance levels** using 
+      volume-weighted moving averages and 
+      volatility metrics.
+
+    Formula:
+    1. Compute VWMA (**Volume-Weighted Moving Average**) over 
+      fast and slow periods.
+    2. Calculate **Volume Price Confirmation (VPC)** to 
+      measure price deviations.
+    3. Compute **Volume Price Ratio (VPR)** by dividing fast 
+      VWMA by a simple moving average.
+    4. Determine **Volume Momentum (VM)** by comparing fast and 
+      slow volume trends.
+    5. Combine VPC, VPR, and VM into **VPCI** for 
+      trend confirmation.
+    6. Apply a **custom price function** to adjust 
+      support/resistance levels.
+    7. Use a smoothed moving average (**SMA**) to 
+      finalize AVSL levels.
+
+    Usage:
+    - Identifies **strong support zones** based on 
+      price and volume dynamics.
+    - Provides adaptive thresholds for **trend 
+      validation and risk management**.
+    """
+
+    def calculate_avsl(
+        self, 
+        data: PriceDataFrame, 
+        config: AvslConfigDM
+    ) -> pd.DataFrame:
+        """
+        Computes the AVSL (Adaptive Support Level) values.
+        Args:
+            data (PriceDataFrame): Market price dataset containing 
+              high, low, close prices and volume.
+            config (AvslConfigDM): AVSL configuration parameters.
+        Returns:
+            pd.DataFrame: A DataFrame containing AVSL values 
+              indexed by date.
+        """
+        # Compute core components (VWMA, VPC, VPR, VM, VPCI)
+        vw_f, vw_s, vpc, vpr, vm, vpci = self._compute_base_series(data, config)
+        # Compute price adjustment and deviation
+        price_v = self._price_fun(data, vpc, vpr, vpci)
+        deviation = config.stand_div * vpci * vm
+        # Compute AVSL using SMA
+        avsl = ta.sma(
+            close=data.low_prices - price_v + deviation,
+            length=config.length_slow, 
+            talib=True
+        )
+        return pd.DataFrame(
+            {"avsl": avsl}, 
+            index=data.index
+        )
+
+    def _compute_base_series(
+        self, 
+        data: PriceDataFrame, 
+        config: AvslConfigDM
+    ) -> tuple[NDArray]:
+        """
+        Computes base technical indicators required 
+          for AVSL.
+        Args:
+            data (PriceDataFrame): Market price dataset.
+            config (AvslConfigDM): AVSL configuration 
+              parameters.
+        Returns:
+            Tuple[np.ndarray]: VWMA fast, VWMA slow, 
+              VPC, VPR, VM, VPCI.
+        """
+        vw_ma_fast = ta.vwma(
+            close=data.close_prices, 
+            volume=data.volumes, 
+            length=config.length_fast
+        )
+        vw_ma_slow = ta.vwma(
+            close=data.close_prices, 
+            volume=data.volumes, 
+            length=config.length_slow
+        )
+        vpc = vw_ma_slow - ta.sma(
+            close=data.close_prices, 
+            length=config.length_slow, 
+            talib=True
+        )
+        vpr = vw_ma_fast / ta.sma(
+            close=data.close_prices, 
+            length=config.length_fast, 
+            talib=True
+        )
+        vm = ta.sma(
+            close=data.volumes, 
+            length=config.length_fast, 
+            talib=True
+        ) / ta.sma(
+            close=data.volumes, 
+            length=config.length_slow, 
+            talib=True
+        )
+        return (
+            vw_ma_fast, 
+            vw_ma_slow, 
+            vpc, vpr, vm, 
+            vpc * vpr * vm
+        )
+
+    def _price_fun(
+        self, 
+        data: PriceDataFrame, 
+        vpc: pd.Series, 
+        vpr: pd.Series, 
+        vpci: pd.Series
+    ) -> NDArray:
+        """
+        Optimized price adjustment function using vectorized 
+          calculations and a Numba-jitted helper.
+        Args:
+            data (PriceDataFrame): Market price dataset.
+            vpc (pd.Series): Volume Price Confirmation values.
+            vpr (pd.Series): Volume Price Ratio values.
+            vpci (pd.Series): VPCI values.
+        Returns:
+            np.ndarray: Adjusted price levels based on 
+              volume-price interaction.
+        """
+        # Convert pandas Series to NumPy arrays
+        # for faster processing
+        low_np, vpc_np, vpr_np, vpci_np = (
+            s.to_numpy() for s in (data.low_prices, vpc, vpr, vpci)
+        )
+        # Compute `lenV` (Volume Confirmation Length)
+        lenV = self.compute_len_v(vpc_np, vpci_np)
+        # Compute `VPCc` (Adjusted Volume Confirmation Coefficient)
+        VPCc = self.compute_vpcc(vpc_np)
+        # Call the optimized JIT helper function
+        return _compute_price_v(low_np, vpr_np, lenV, VPCc)
+
+    @staticmethod
+    def compute_len_v(
+        vpc: NDArray, 
+        vpci: NDArray
+    ) -> NDArray:
+        """
+        Computes volume confirmation length (`lenV`) 
+          using vectorized calculations.
+        Args:
+            vpc (np.ndarray): Volume Price 
+              Confirmation values.
+            vpci (np.ndarray): VPCI values.
+        Returns:
+            np.ndarray: Computed length values.
+        """
+        return np.where(
+            # Prevent division by zero
+            np.isnan(vpci), 1,  
+            np.where(
+                vpc < 0,
+                np.round(np.abs(vpci - 3)).astype(np.int32),
+                np.round(vpci + 3).astype(np.int32),
+            )
+        )
+
+    @staticmethod
+    def compute_vpcc(vpc: np.ndarray) -> np.ndarray:
+        """
+        Computes adjusted Volume Price 
+          Confirmation Coefficient (`VPCc`).
+        Args:
+            vpc (np.ndarray): Volume Price Confirmation values.
+        Returns:
+            np.ndarray: Adjusted VPC coefficients.
+        """
+        return np.where(
+            (vpc > -1) & (vpc < 0),
+            -1.0,
+            np.where(
+                (
+                    vpc >= 0
+                ) & (
+                    vpc < 1
+                ),
+                1.0, vpc
+            ),
+        )
+
+    def get_last_avsl_signal(
+        self, 
+        data: PriceDataFrame, 
+        config: AvslConfigDM
+    ) -> float | None:
+        """
+        Retrieves the AVSL value on the last bar.
+        Args:
+            data (PriceDataFrame): Market data with price information.
+            config (AvslConfigDM): Configuration settings for AVSL.
+        Returns:
+            float | None: AVSL value of the last bar 
+              if available, otherwise None.
+        """
+        # Compute AVSL values
+        avsl_df = self.calculate_avsl(data, config)
+        # Ensure AVSL is not empty before retrieving the last value
+        if avsl_df.empty or "avsl" not in avsl_df.columns or pd.isna(avsl_df["avsl"].iloc[-1]):
+            return None
+        return avsl_df["avsl"].iloc[-1]
+
+
+@njit
+def _compute_price_v(
+    low: NDArray, 
+    vpr: NDArray, 
+    lenV: NDArray, 
+    VPCc: NDArray
+) -> NDArray:
+    """
+    Optimized price computation using Numba JIT for speed.
+    Args:
+        low (np.ndarray): Array of low prices.
+        vpr (np.ndarray): Volume Price Ratio.
+        lenV (np.ndarray): Length value for price calculation.
+        VPCc (np.ndarray): Adjusted VPC coefficient.
+    Returns:
+        np.ndarray: Optimized price values.
+    """
+    n = low.shape[0]
+    out = np.empty(n, dtype=np.float64)
+    for i in range(n):
+        L = lenV[i]
+        if L > 0:
+            s = np.sum(
+                np.divide(
+                    low[max(0, i - L + 1): i + 1],
+                    VPCc[i] * vpr[max(0, i - L + 1): i + 1],
+                    out=np.zeros_like(
+                        low[max(0, i - L + 1): i + 1]
+                    ),
+                    # Prevent division by zero
+                    where=(
+                        VPCc[i] != 0
+                    ) & (
+                        vpr[max(0, i - L + 1): i + 1] != 0
+                    )  
+                )
+            )
+            out[i] = s / L / 100.0
+        else:
+            out[i] = low[i]
+    return out
diff --git a/strategies/src/infrastructure/indicators/order_block.py b/strategies/src/infrastructure/indicators/order_block.py
new file mode 100644
index 0000000..7e0daf1
--- /dev/null
+++ b/strategies/src/infrastructure/indicators/order_block.py
@@ -0,0 +1,85 @@
+import numpy as np
+import pandas as pd
+from scipy.signal import find_peaks  # type: ignore
+
+from strategies.src.domain.entities import OrderBlockDetectorDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class OrderBlockDetector:
+    """
+    Order Block Detector Using ZigZag Peaks.
+
+    This class detects order blocks by identifying 
+      **peaks** and **valleys** in price 
+      data using the ZigZag pattern algorithm.
+
+    Formula:
+    1. Find **peaks** using `find_peaks()` applied 
+      to **high prices**.
+    2. Find **valleys** using `find_peaks()` applied 
+      to **low prices**.
+    3. Define peak prominence, threshold, and 
+      distance to filter out weak signals.
+    4. Store peak and valley locations in separate arrays.
+    5. Return a DataFrame marking peaks 
+      and valleys for further analysis.
+
+    Usage:
+    - Helps identify **key support 
+      and resistance levels**.
+    - Detects **potential reversal zones** for 
+      price action traders.
+    - Useful for refining entry and 
+      exit points in trading strategies.
+    """
+
+    def zigzag_indicator(
+        self, 
+        data: PriceDataFrame, 
+        config: OrderBlockDetectorDM
+    ) -> pd.DataFrame:
+        """
+        Detects peaks and valleys in price data 
+          using ZigZag pattern analysis.
+        Args:
+            data (PriceDataFrame): Market price dataset 
+              containing high and low prices.
+            config (OrderBlockDetectorDM): Configuration 
+              settings for peak detection.
+        Returns:
+            pd.DataFrame: A DataFrame with marked peaks and valleys.
+        """
+        # Detect peaks (local maxima) in high prices
+        peaks, _ = find_peaks(
+            x=data.high_prices, 
+            height=config.height,
+            threshold=config.threshold,
+            distance=config.distance,
+            prominence=config.peak_prominance,
+            width=config.width,
+            wlen=config.wlen,
+            rel_height=config.rel_height,
+            plateau_size=config.plateu_size
+        )
+        # Detect valleys (local minima) in low prices
+        valleys, _ = find_peaks(
+            x=data.low_prices, 
+            height=config.height,
+            threshold=config.threshold,
+            distance=config.distance,
+            prominence=config.valley_prominance,
+            width=config.width,
+            wlen=config.wlen,
+            rel_height=config.rel_height,
+            plateau_size=config.plateu_size
+        )
+        # Initialize arrays to store detected peaks and valleys
+        peaks_array = np.full_like(data.high_prices, np.nan)
+        valleys_array = np.full_like(data.low_prices, np.nan)
+        peaks_array[peaks] = data.high_prices.iloc[peaks]
+        valleys_array[valleys] = data.low_prices.iloc[valleys]
+        return pd.DataFrame(
+            {"peaks": peaks_array, "valleys": valleys_array},
+            index=data.index
+        )
diff --git a/strategies/src/infrastructure/indicators/rsi_clouds.py b/strategies/src/infrastructure/indicators/rsi_clouds.py
index 9a9dd39..86c88ee 100644
--- a/strategies/src/infrastructure/indicators/rsi_clouds.py
+++ b/strategies/src/infrastructure/indicators/rsi_clouds.py
@@ -3,38 +3,76 @@
 import pandas_ta as ta
 
 from strategies.src.domain.entities import RsiCloudsConfigDM
-from strategies.src.infrastructure.types import PriceDataFrame
+from strategies.src.infrastructure._types import PriceDataFrame
 
 
-class CloudsRsi:
+class RsiClouds:
+    """
+    RSI Clouds Indicator Implementation.
+
+    The RSI Clouds technique integrates 
+      **Relative Strength Index (RSI)** and **MACD** 
+      to provide momentum-based trend insights.
+
+    Formula:
+    1. Compute the **average price** using OHLC values.
+    2. Calculate **RSI** based on the average price to track momentum.
+    3. Apply **MACD** to RSI to highlight trend shifts.
+    4. Generate trading signals based on:
+       - **MACD line crossing its signal line** (entry/exit points).
+       - **MACD histogram crossing zero** (trend confirmation).
+    Usage:
+    - Helps traders visualize **momentum clouds** around RSI.
+    - Combines two powerful indicators (RSI + MACD) for enhanced signal accuracy.
+    """
+
     def prepare_data(self, data: PriceDataFrame) -> pd.Series:
-        """Создание среднего ценового ряда."""
+        """
+        Creates the average price series from OHLC values.
+        Args:
+            data (PriceDataFrame): Market price dataset.
+        Returns:
+            pd.Series: A time series representing the 
+              average price.
+        """
+        # Computes the mean price from open, high, low, and close.
         return (
-            data.low_price + 
-            data.high_price + 
+            data.low_prices + 
+            data.high_prices + 
             data.open_price + 
-            data.close_price
-        ) / 4
+            data.close_prices
+        ) / 4  
 
-    def calculate(
+    def calculate_rsi_clouds(
         self, 
         data: PriceDataFrame, 
         config: RsiCloudsConfigDM
     ) -> pd.DataFrame:
-        """Расчет RSI и MACD."""
+        """
+        Computes RSI and MACD based on the average price.
+        Args:
+            data (PriceDataFrame): Market price dataset.
+            config (RsiCloudsConfigDM): RSI Clouds 
+              configuration settings.
+        Returns:
+            pd.DataFrame: A DataFrame containing 
+              RSI values and MACD indicators.
+        """
+        # Prepare average price data
         ohlc = pd.DataFrame(
             data={"avg": self.prepare_data(data)}, 
             index=data.date
         )
+        # Compute RSI based on average price
         ohlc['rsi'] = ta.rsi(
             close=ohlc["avg"], 
             length=config.rsi_length, 
             scalar=config.scalar,
             drift=config.drift,
             offset=config.offset, 
-            talib=config.talib, 
+            talib=config.talib
         )
-        
+        # Compute MACD based on RSI values
         macd_ind = ta.macd(
             close=ohlc['rsi'],
             fast=config.macd_fast, 
@@ -43,20 +81,35 @@ def calculate(
             talib=config.talib, 
             offset=config.offset
         )
+        # Rename MACD components for clarity
         suffixes = {"": "macd_line", "s": "macd_signal", "h": "histogram"}
         ohlc = ohlc.join(macd_ind.rename(columns={
             f"MACD{suffix}_{config.macd_fast}_"
             f"{config.macd_slow}_{config.macd_signal}": name
             for suffix, name in suffixes.items()
         }))
-        del macd_ind
+        # Free up memory
+        del macd_ind  
         return ohlc
 
     def create_signals(self, ohlc: pd.DataFrame) -> pd.DataFrame:
-        """Создание сигналов на основе пересечения MACD."""
+        """
+        Generates buy/sell signals based on MACD crossovers.
+        Signals:
+        - **Buy:** MACD crosses **above** the signal line.
+        - **Sell:** MACD crosses **below** the signal line.
+        - **Histogram crosses zero:** Confirms trend shift.
+        Args:
+            ohlc (pd.DataFrame): DataFrame 
+              containing RSI and MACD values.
+        Returns:
+            pd.DataFrame: A DataFrame 
+              with trading signals.
+        """
+        # Extract previous MACD values for crossover detection
         prev_macd_line = ohlc['macd_line'].shift(1)
         prev_macd_signal = ohlc['macd_signal'].shift(1)
-        
+        # Identify MACD signal line crossovers
         ohlc['macd_cross_signal'] = np.select(
             [
                 (
@@ -70,27 +123,39 @@ def create_signals(self, ohlc: pd.DataFrame) -> pd.DataFrame:
                     ohlc['macd_line'] < ohlc['macd_signal']
                 ),
             ],
-            [1, -1],
+            # 1 → Buy, -1 → Sell
+            [1, -1],  
             default=0
         )
+        # Identify histogram crossing zero
         ohlc['histogram_cross_zero'] = np.select(
             [
                 (ohlc['histogram'].shift(1) < 0) & (ohlc['histogram'] > 0),
                 (ohlc['histogram'].shift(1) > 0) & (ohlc['histogram'] < 0),
             ],
-            [1, -1],
+            # 1 → Uptrend confirmation, -1 → Downtrend confirmation
+            [1, -1],  
             default=0
         )
         return ohlc
 
-    def get_last_signal(self, ohlc: pd.DataFrame) -> str:
-        """Определяет последний сигнал MACD (без учета гистограммы)."""
+    def get_last_signal(self, ohlc: pd.DataFrame) -> str | None:
+        """
+        Retrieves the last MACD crossover signal.
+        Args:
+            ohlc (pd.DataFrame): DataFrame containing 
+              RSI and MACD values.
+        Returns:
+            str | None: "buy" if MACD crossed above, "sell" if 
+              MACD crossed below, or None if no signal.
+        """
+        # Ensure data is available
         if ohlc.empty:
-            return "None"
+            return None
         last_macd_signal = ohlc['macd_cross_signal'].iloc[-1]
         if last_macd_signal == 1:
-            return "Buy"
+            return "buy"
         elif last_macd_signal == -1:
-            return "Sell"
+            return "sell"
         else:
-            return "None"
+            return None
diff --git a/strategies/src/infrastructure/indicators/scrsi.py b/strategies/src/infrastructure/indicators/scrsi.py
new file mode 100644
index 0000000..451ad10
--- /dev/null
+++ b/strategies/src/infrastructure/indicators/scrsi.py
@@ -0,0 +1,201 @@
+import pandas as pd
+import numpy as np
+
+from strategies.src.domain.entities import ScrsiConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class SmoothCicleRsi:
+    """
+    SmoothCicle RSI (SCRSI) Indicator Implementation.
+
+    This indicator calculates an adaptive RSI that includes 
+    smoothing techniques to filter noise.
+    It measures price momentum based on the 
+    difference between closing prices over a 
+    set cycle length.
+
+    Formula:
+    1. Calculate price difference (`diff = close_prices.diff()`).
+    2. Compute the average of positive and negative movements:
+       - up = average gain over `cyclelen` periods.
+       - down = average loss over `cyclelen` periods.
+    3. Standard RSI formula: RSI = 100 - (100 / (1 + up/down)).
+    4. Convert RSI from range [0,100] to [-100,100] to highlight trends.
+    5. Apply cyclic smoothing to refine RSI momentum shifts.
+    6. Define boundaries (`Lower Bound` and `Upper Bound`)
+       based on percentile thresholds.
+
+    Usage:
+    - Provides momentum-based trading signals.
+    - Generates BUY/SELL signals based on 
+      RSI thresholds (crossing level 50, reversals from 0/100).
+    """
+
+    def calculate_scrsi(
+        self, 
+        data: PriceDataFrame, 
+        config: ScrsiConfigDM
+    ) -> pd.DataFrame:
+        """
+        Computes the SmoothCicle RSI (SCRSI) values.
+        Args:
+            data (PriceDataFrame): The price dataset containing close prices.
+            config (ScrsiConfigDM): Indicator configuration (cycle length, 
+            vibration, smoothing).
+        Returns:
+            pd.DataFrame: A DataFrame containing the SCRSI 
+            scaled values and boundary levels.
+        """
+        # Compute cycle length and cyclic memory for smoothing
+        cyclelen = config.domcycle // 2
+        cyclicmemory = config.domcycle * 2
+        # Calculate RSI components: Up (gains) and Down (losses)
+        up = pd.Series(
+            np.maximum(
+                data.close_prices.diff(), 0
+            )
+        ).rolling(window=cyclelen).mean()
+        down = pd.Series(
+            np.minimum(
+                data.close_prices.diff(), 0
+            )
+        ).rolling(window=cyclelen).mean()
+        # Standard RSI calculation with safe division 
+        # (to avoid division by zero issues)
+        rsi = 100 - 100 / (1 + np.divide(
+            up, down, 
+            out=np.zeros_like(up), 
+            where=down != 0)
+        )
+        # Ensure RSI stays within [0, 100]
+        rsi = np.clip(rsi, 0, 100)
+        # Convert RSI scale from [0,100] to [-100,100]
+        # for better trend visibility
+        rsi_scaled = (rsi - 50) * 2
+        # Apply cyclic smoothing to RSI
+        torque = 2.0 / (config.vibration + 1)
+        phasingLag = (config.vibration - 1) // 2
+        crsi = np.zeros_like(rsi_scaled)
+        # Apply smoothing logic using vectorization
+        mask = np.arange(len(rsi_scaled)) >= phasingLag
+        crsi[mask] = torque * (
+            2 * rsi_scaled[mask] - np.roll(
+                rsi_scaled, phasingLag
+            )[mask]
+        ) + (1 - torque) * np.roll(crsi, 1)[mask]
+        # Remove NaN values before calculating boundary levels
+        crsi_clean = crsi[~np.isnan(crsi)]
+        # Define lower and upper bounds using percentile thresholds
+        db = float(
+            np.percentile(
+                crsi_clean[:cyclicmemory], config.leveling
+            ) if len(
+                crsi_clean[:cyclicmemory]
+            ) > 0 else -100
+        )
+        ub = float(
+            np.percentile(
+                crsi_clean[:cyclicmemory], 
+                100 - config.leveling
+            ) if len(
+                crsi_clean[:cyclicmemory]
+            ) > 0 else 100
+        )
+        # Ensure arrays are 1D before storing results 
+        # without misaligning indices
+        if getattr(crsi, "ndim", 1) > 1:
+            crsi = crsi.flatten()
+        if getattr(rsi_scaled, "ndim", 1) > 1:
+            rsi_scaled = rsi_scaled.flatten()
+        return pd.DataFrame({
+            'CRSI Scaled': rsi_scaled,
+            'Lower Bound': db,
+            'Upper Bound': ub
+        }, index=data.index)
+
+    def generate_scrsi_signals(
+        self, 
+        data: pd.DataFrame
+    ) -> pd.DataFrame:
+        """
+        Generates trading signals based on SCRSI levels.
+        Signals:
+        - Buy: RSI crossing above level 50 or bouncing from 0 (oversold zone).
+        - Sell: RSI crossing below level 50 or bouncing from 100 (overbought zone).
+        Args:
+            data (pd.DataFrame): DataFrame containing SCRSI values.
+        Returns:
+            pd.DataFrame: A DataFrame with separate buy and sell signal series.
+        """
+        # Create a series for BUY signals (0 = no signal, 1 = buy)
+        buy_signals = pd.Series(0, index=data.index)
+        # Buy signal: Crossing 50 from below
+        buy_signals[
+            (
+                data["CRSI Scaled"].shift(1) < 50
+            ) & (
+                data["CRSI Scaled"] >= 50
+            )
+        ] = 1  
+        # Buy signal: Reversal from oversold region (0)
+        buy_signals[
+            (
+                data["CRSI Scaled"].shift(1) <= 0
+            ) & (
+                data["CRSI Scaled"] > 0
+            )
+        ] = 1
+        # Create a series for SELL signals (0 = no signal, 1 = sell)
+        sell_signals = pd.Series(0, index=data.index)
+        # Sell signal: Crossing 50 from above
+        sell_signals[
+            (
+                data["CRSI Scaled"].shift(1) > 50
+            ) & (
+                data["CRSI Scaled"] <= 50
+            )
+        ] = 1
+        # Sell signal: Reversal from overbought region (100)
+        sell_signals[
+            (
+                data["CRSI Scaled"].shift(1) >= 100
+            ) & (
+                data["CRSI Scaled"] < 100
+            )
+        ] = 1  
+        return pd.DataFrame(
+            {
+                "buy_signals": buy_signals, 
+                "sell_signals": sell_signals
+            }, 
+            index=data.index
+        )
+
+    def get_last_signal(
+        self, 
+        data: PriceDataFrame, 
+        config: ScrsiConfigDM
+    ) -> str | None:
+        """
+        Determines the last trading signal based on SCRSI.
+        - If the last bar has a buy signal → returns "long".
+        - If the last bar has a sell signal → returns "short".
+        - If no signal is detected → returns None.
+        Args:
+            data (PriceDataFrame): Market data.
+            config (ScrsiConfigDM): SCRSI indicator configuration.
+        Returns:
+            str | None: "long" if buy signal, "short" if sell signal, 
+            or None if no signal is present.
+        """
+        # Compute SCRSI values
+        scrsi_df = self.calculate_scrsi(data, config)
+        # Generate trading signals
+        signals_df = self.generate_scrsi_signals(scrsi_df)
+        # Check the last signal and return appropriate action
+        if signals_df["buy_signals"].iloc[-1] == 1:
+            return "long"
+        elif signals_df["sell_signals"].iloc[-1] == 1:
+            return "short"
+        return None
diff --git a/strategies/src/infrastructure/indicators/stoch_rsi.py b/strategies/src/infrastructure/indicators/stoch_rsi.py
new file mode 100644
index 0000000..be2e0cf
--- /dev/null
+++ b/strategies/src/infrastructure/indicators/stoch_rsi.py
@@ -0,0 +1,127 @@
+import talib as ta
+from pandas import DataFrame
+import pandas as pd
+import numpy as np
+
+from strategies.src.domain.entities import StochRsiConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class StochRSI:
+    """
+    Stochastic RSI (StochRSI) Indicator Implementation.
+
+    The Stochastic RSI measures the relative position of RSI
+    compared to its high-low range over a defined period. 
+    It is commonly used to identify potential overbought 
+    and oversold conditions.
+
+    Formula:
+    1. Calculate the **Relative Strength Index (RSI)** over `timeperiod`.
+    2. Compute the **lowest and highest RSI values** over the `fastk_period` window.
+    3. Normalize RSI within this range:
+       - %K = (RSI - Lowest RSI) / (Highest RSI - Lowest RSI) * 100.
+    4. Apply a smoothing Moving Average (`fastd_period`) to %K to get **%D**.
+    5. Generate trading signals based on crossings:
+       - %K crosses above %D in oversold conditions (**Buy Signal**).
+       - %K crosses below %D in overbought conditions (**Sell Signal**).
+
+    Usage:
+    - Provides **momentum-based trading signals**.
+    - Helps detect **trend reversals in extreme conditions (oversold/overbought)**.
+    """
+
+    def calculate_stochrsi(
+            self, 
+            data: PriceDataFrame, 
+            config: StochRsiConfigDM
+        ) -> DataFrame:
+        """
+        Computes the Stochastic RSI (%K and %D) values.
+        Args:
+            data (PriceDataFrame): The price dataset containing closing prices.
+            config (StochRsiConfigDM): Configuration for Stochastic RSI 
+                (time period, smoothing factors).
+        Returns:
+            DataFrame: A DataFrame containing the %K and %D values.
+        """
+        # Compute the Stochastic RSI components using TA-Lib
+        fastk, fastd = ta.STOCHRSI(
+            close=data.close_prices,
+            # RSI calculation period
+            timeperiod=config.timeperiod,
+            # %K period (high-low RSI range)
+            fastk_period=config.fastk_period,
+            # %D smoothing period  
+            fastd_period=config.fastd_period, 
+            # Moving Average type for %D 
+            fastd_matype=config.fastd_matype  
+        )
+        return DataFrame({"fastk": fastk, "fastd": fastd}, index=data.index)
+
+    def create_signals(self, data: DataFrame) -> DataFrame:
+        """
+        Generates buy/sell signals based on Stochastic RSI crossovers.
+        Signals:
+        - Buy: When %K crosses above %D in oversold 
+            conditions (**fastk < 20**).
+        - Sell: When %K crosses below %D in overbought 
+            conditions (**fastk > 80**).
+        Args:
+            data (DataFrame): DataFrame containing %K and %D values.
+        Returns:
+            DataFrame: A DataFrame with separate buy and sell signal series.
+        """
+        # Buy signal: %K crosses above %D **and** is 
+        # in oversold territory (<20)
+        buy_signals = (
+            data['fastk'] > data['fastd']
+        ) & (
+            data['fastk'].shift(1) < data['fastd'].shift(1)
+        ) & (
+            data['fastk'] < 20
+        )
+        # Sell signal: %K crosses below %D **and** is 
+        # in overbought territory (>80)
+        sell_signals = (
+            data['fastk'] < data['fastd']
+        ) & (
+            data['fastk'].shift(1) > data['fastd'].shift(1)
+        ) & (
+            data['fastk'] > 80
+        )
+        return DataFrame(
+            {
+                "buy_signals": buy_signals.astype(int), 
+                "sell_signals": sell_signals.astype(int)
+            }, 
+            index=data.index
+        )
+
+    def get_last_signal(
+        self, 
+        data: PriceDataFrame, 
+        config: StochRsiConfigDM
+    ) -> str | None:
+        """
+        Determines the last trading signal based on Stochastic RSI.
+        - If the last bar has a buy signal → returns "long".
+        - If the last bar has a sell signal → returns "short".
+        - If no signal is detected → returns None.
+        Args:
+            data (PriceDataFrame): Market data with closing prices.
+            config (StochRsiConfigDM): Stochastic RSI configuration.
+        Returns:
+            str | None: "long" if buy signal, "short" if sell signal,
+              or None if no signal is present.
+        """
+        # Compute Stochastic RSI values
+        stoch_rsi_df = self.calculate_stochrsi(data, config)
+        # Generate trading signals
+        signals_df = self.create_signals(stoch_rsi_df)
+        # Check the last signal and return appropriate action
+        if signals_df["buy_signals"].iloc[-1] == 1:
+            return "long"
+        elif signals_df["sell_signals"].iloc[-1] == 1:
+            return "short"
+        return None
diff --git a/uv.lock b/uv.lock
index bdd695e..c5f8870 100644
--- a/uv.lock
+++ b/uv.lock
@@ -178,6 +178,19 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload_time = "2025-03-13T11:10:21.14Z" },
 ]
 
+[[package]]
+name = "beautifulsoup4"
+version = "4.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "soupsieve" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload_time = "2025-04-15T17:05:13.836Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload_time = "2025-04-15T17:05:12.221Z" },
+]
+
 [[package]]
 name = "certifi"
 version = "2025.4.26"
@@ -220,6 +233,41 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload_time = "2024-09-04T20:44:45.309Z" },
 ]
 
+[[package]]
+name = "charset-normalizer"
+version = "3.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload_time = "2025-05-02T08:34:42.01Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload_time = "2025-05-02T08:32:33.712Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload_time = "2025-05-02T08:32:35.768Z" },
+    { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload_time = "2025-05-02T08:32:37.284Z" },
+    { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload_time = "2025-05-02T08:32:38.803Z" },
+    { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload_time = "2025-05-02T08:32:40.251Z" },
+    { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload_time = "2025-05-02T08:32:41.705Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload_time = "2025-05-02T08:32:43.709Z" },
+    { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload_time = "2025-05-02T08:32:46.197Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload_time = "2025-05-02T08:32:48.105Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload_time = "2025-05-02T08:32:49.719Z" },
+    { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload_time = "2025-05-02T08:32:51.404Z" },
+    { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload_time = "2025-05-02T08:32:53.079Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload_time = "2025-05-02T08:32:54.573Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload_time = "2025-05-02T08:32:56.363Z" },
+    { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload_time = "2025-05-02T08:32:58.551Z" },
+    { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload_time = "2025-05-02T08:33:00.342Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload_time = "2025-05-02T08:33:02.081Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload_time = "2025-05-02T08:33:04.063Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload_time = "2025-05-02T08:33:06.418Z" },
+    { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload_time = "2025-05-02T08:33:08.183Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload_time = "2025-05-02T08:33:09.986Z" },
+    { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload_time = "2025-05-02T08:33:11.814Z" },
+    { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload_time = "2025-05-02T08:33:13.707Z" },
+    { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload_time = "2025-05-02T08:33:15.458Z" },
+    { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload_time = "2025-05-02T08:33:17.06Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload_time = "2025-05-02T08:33:18.753Z" },
+    { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload_time = "2025-05-02T08:34:40.053Z" },
+]
+
 [[package]]
 name = "click"
 version = "8.1.8"
@@ -241,6 +289,47 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" },
 ]
 
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload_time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload_time = "2025-04-15T17:37:03.105Z" },
+    { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload_time = "2025-04-15T17:37:07.026Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload_time = "2025-04-15T17:37:11.481Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload_time = "2025-04-15T17:37:18.212Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload_time = "2025-04-15T17:37:22.76Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload_time = "2025-04-15T17:37:33.001Z" },
+    { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload_time = "2025-04-15T17:37:48.64Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload_time = "2025-04-15T17:38:06.7Z" },
+    { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload_time = "2025-04-15T17:38:10.338Z" },
+    { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload_time = "2025-04-15T17:38:14.239Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload_time = "2025-04-15T17:38:19.142Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload_time = "2025-04-15T17:38:23.688Z" },
+    { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload_time = "2025-04-15T17:38:28.238Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload_time = "2025-04-15T17:38:33.502Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload_time = "2025-04-15T17:38:38.672Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload_time = "2025-04-15T17:38:43.712Z" },
+    { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload_time = "2025-04-15T17:39:00.224Z" },
+    { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload_time = "2025-04-15T17:43:29.649Z" },
+    { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload_time = "2025-04-15T17:44:44.532Z" },
+    { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload_time = "2025-04-15T17:44:48.194Z" },
+    { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload_time = "2025-04-15T17:43:34.084Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload_time = "2025-04-15T17:43:38.626Z" },
+    { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload_time = "2025-04-15T17:43:44.522Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload_time = "2025-04-15T17:43:49.545Z" },
+    { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload_time = "2025-04-15T17:43:54.203Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload_time = "2025-04-15T17:44:01.025Z" },
+    { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload_time = "2025-04-15T17:44:17.322Z" },
+    { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload_time = "2025-04-15T17:44:33.43Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload_time = "2025-04-15T17:44:37.092Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload_time = "2025-04-15T17:44:40.827Z" },
+]
+
 [[package]]
 name = "cryptography"
 version = "44.0.2"
@@ -276,6 +365,36 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957, upload_time = "2025-03-02T00:01:06.987Z" },
 ]
 
+[[package]]
+name = "curl-cffi"
+version = "0.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "cffi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/c0/18e408334c480f2cacca02feec8cd01dba9e3f1637cd03fd21b033c2bfa3/curl_cffi-0.10.0.tar.gz", hash = "sha256:3e37b35268ca58492f54ed020ae4b50c33ee0debad4145db9f746f04ed466eb0", size = 144434, upload_time = "2025-03-12T12:05:12.691Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/56/6c/f3a27db82305540ecab0db688a5977162ed7d64e274c09ea7c9ed3f4a541/curl_cffi-0.10.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:15053d01c6a3e3c4c5331ce9e07e1dc31ca5aa063babca05d18b1b5aad369fac", size = 5224229, upload_time = "2025-03-12T12:04:58.213Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/cc/5dc45cf5f52c82979daef5b414108a39f9eeaa164a6810da1368d307b468/curl_cffi-0.10.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3969e4260ad4dab638fb6dbe349623f9f5f022435c7fd21daf760231380367fa", size = 2700826, upload_time = "2025-03-12T12:05:00.765Z" },
+    { url = "https://files.pythonhosted.org/packages/f7/58/fff44c797abf4a101723f167259801cde5d7bcfc7621bb37baeeb3e03fdc/curl_cffi-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:458f53c41bd76d90d8974d60c3a8a0dd902a1af1f9056215cf24f454bcedc6fd", size = 7067933, upload_time = "2025-03-12T12:05:02.168Z" },
+    { url = "https://files.pythonhosted.org/packages/24/1f/a51248f89b958216cf29342b8a43befefc62301781576693b25caea5c24f/curl_cffi-0.10.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfc74f09e44d2d8d61b8e8fda3a7004b5bc0217a703fbbe9e16ef8caa1f3d4e4", size = 6624491, upload_time = "2025-03-12T12:05:03.634Z" },
+    { url = "https://files.pythonhosted.org/packages/09/50/028ab0c0ef9717af285801e7ba5b1d83e42073e06d2c62c7a9e8a0b9a0fa/curl_cffi-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f03f4b17dc679c82bd3c946feb1ad38749b2ad731d7c26daefaac857d1c72fd9", size = 7447104, upload_time = "2025-03-12T12:05:05.097Z" },
+    { url = "https://files.pythonhosted.org/packages/07/54/0406653d8dbcfa766fe33c43aa19438003bdee03119cb9c1b070b03a10ee/curl_cffi-0.10.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f1b0c7b7b81afca15a0e56c593d3c2bdcd4fd4c9ca49b9ded5b9d8076ba78ff9", size = 7892220, upload_time = "2025-03-12T12:05:06.553Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/68/a83946686cd96f87f1611b26dbee10dc41c7dbfc2558c8f4c762caf975c5/curl_cffi-0.10.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:04b1d23f0f54f94b8298ed417e6bece85a635d674723cde2b155da686efbf78f", size = 7831954, upload_time = "2025-03-12T12:05:08.073Z" },
+    { url = "https://files.pythonhosted.org/packages/20/0f/78b0221782c43cba88cb09b74484bc0fc7db09c85ddefaf9817381a9414f/curl_cffi-0.10.0-cp39-abi3-win32.whl", hash = "sha256:1e60b8ecc80bfb0da4ff73ac9d194e80482b50ecbb8aefec1b0edaf45fafd80e", size = 1087075, upload_time = "2025-03-12T12:05:09.656Z" },
+    { url = "https://files.pythonhosted.org/packages/26/80/dbc3a5119afe233ddc9dfc14d5b175636443e5d6349f4cdce3eaa9e1523a/curl_cffi-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:59389773a1556e087120e91eac1e33f84f1599d853e1bc168b153e4cdf360002", size = 1374008, upload_time = "2025-03-12T12:05:11.408Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload_time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload_time = "2023-10-07T05:32:16.783Z" },
+]
+
 [[package]]
 name = "dishka"
 version = "1.5.3"
@@ -335,6 +454,42 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/18/41/ce4c906923492cc93b9cf58ab3136231a3d52b531d3f86f97b5c83ef6e7f/faststream-0.5.39-py3-none-any.whl", hash = "sha256:5928e9e85d3e2a06a0780e068b0ddc1081a13c802789e0684df3971dbcc816e2", size = 392881, upload_time = "2025-04-10T07:41:26.215Z" },
 ]
 
+[[package]]
+name = "fonttools"
+version = "4.57.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/03/2d/a9a0b6e3a0cf6bd502e64fc16d894269011930cabfc89aee20d1635b1441/fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de", size = 3492448, upload_time = "2025-04-03T11:07:13.898Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cb/98/d4bc42d43392982eecaaca117d79845734d675219680cd43070bb001bc1f/fonttools-4.57.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:889e45e976c74abc7256d3064aa7c1295aa283c6bb19810b9f8b604dfe5c7f31", size = 2751824, upload_time = "2025-04-03T11:06:03.782Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/62/7168030eeca3742fecf45f31e63b5ef48969fa230a672216b805f1d61548/fonttools-4.57.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0425c2e052a5f1516c94e5855dbda706ae5a768631e9fcc34e57d074d1b65b92", size = 2283072, upload_time = "2025-04-03T11:06:05.533Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/82/121a26d9646f0986ddb35fbbaf58ef791c25b59ecb63ffea2aab0099044f/fonttools-4.57.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44c26a311be2ac130f40a96769264809d3b0cb297518669db437d1cc82974888", size = 4788020, upload_time = "2025-04-03T11:06:07.249Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/26/e0f2fb662e022d565bbe280a3cfe6dafdaabf58889ff86fdef2d31ff1dde/fonttools-4.57.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c41ba992df5b8d680b89fd84c6a1f2aca2b9f1ae8a67400c8930cd4ea115f6", size = 4859096, upload_time = "2025-04-03T11:06:09.469Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/44/9075e323347b1891cdece4b3f10a3b84a8f4c42a7684077429d9ce842056/fonttools-4.57.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea1e9e43ca56b0c12440a7c689b1350066595bebcaa83baad05b8b2675129d98", size = 4964356, upload_time = "2025-04-03T11:06:11.294Z" },
+    { url = "https://files.pythonhosted.org/packages/48/28/caa8df32743462fb966be6de6a79d7f30393859636d7732e82efa09fbbb4/fonttools-4.57.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84fd56c78d431606332a0627c16e2a63d243d0d8b05521257d77c6529abe14d8", size = 5226546, upload_time = "2025-04-03T11:06:13.6Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/46/95ab0f0d2e33c5b1a4fc1c0efe5e286ba9359602c0a9907adb1faca44175/fonttools-4.57.0-cp312-cp312-win32.whl", hash = "sha256:f4376819c1c778d59e0a31db5dc6ede854e9edf28bbfa5b756604727f7f800ac", size = 2146776, upload_time = "2025-04-03T11:06:15.643Z" },
+    { url = "https://files.pythonhosted.org/packages/06/5d/1be5424bb305880e1113631f49a55ea7c7da3a5fe02608ca7c16a03a21da/fonttools-4.57.0-cp312-cp312-win_amd64.whl", hash = "sha256:57e30241524879ea10cdf79c737037221f77cc126a8cdc8ff2c94d4a522504b9", size = 2193956, upload_time = "2025-04-03T11:06:17.534Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/2f/11439f3af51e4bb75ac9598c29f8601aa501902dcedf034bdc41f47dd799/fonttools-4.57.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:408ce299696012d503b714778d89aa476f032414ae57e57b42e4b92363e0b8ef", size = 2739175, upload_time = "2025-04-03T11:06:19.583Z" },
+    { url = "https://files.pythonhosted.org/packages/25/52/677b55a4c0972dc3820c8dba20a29c358197a78229daa2ea219fdb19e5d5/fonttools-4.57.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bbceffc80aa02d9e8b99f2a7491ed8c4a783b2fc4020119dc405ca14fb5c758c", size = 2276583, upload_time = "2025-04-03T11:06:21.753Z" },
+    { url = "https://files.pythonhosted.org/packages/64/79/184555f8fa77b827b9460a4acdbbc0b5952bb6915332b84c615c3a236826/fonttools-4.57.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f022601f3ee9e1f6658ed6d184ce27fa5216cee5b82d279e0f0bde5deebece72", size = 4766437, upload_time = "2025-04-03T11:06:23.521Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/ad/c25116352f456c0d1287545a7aa24e98987b6d99c5b0456c4bd14321f20f/fonttools-4.57.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dea5893b58d4637ffa925536462ba626f8a1b9ffbe2f5c272cdf2c6ebadb817", size = 4838431, upload_time = "2025-04-03T11:06:25.423Z" },
+    { url = "https://files.pythonhosted.org/packages/53/ae/398b2a833897297797a44f519c9af911c2136eb7aa27d3f1352c6d1129fa/fonttools-4.57.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dff02c5c8423a657c550b48231d0a48d7e2b2e131088e55983cfe74ccc2c7cc9", size = 4951011, upload_time = "2025-04-03T11:06:27.41Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/5d/7cb31c4bc9ffb9a2bbe8b08f8f53bad94aeb158efad75da645b40b62cb73/fonttools-4.57.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:767604f244dc17c68d3e2dbf98e038d11a18abc078f2d0f84b6c24571d9c0b13", size = 5205679, upload_time = "2025-04-03T11:06:29.804Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/e4/6934513ec2c4d3d69ca1bc3bd34d5c69dafcbf68c15388dd3bb062daf345/fonttools-4.57.0-cp313-cp313-win32.whl", hash = "sha256:8e2e12d0d862f43d51e5afb8b9751c77e6bec7d2dc00aad80641364e9df5b199", size = 2144833, upload_time = "2025-04-03T11:06:31.737Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/0d/2177b7fdd23d017bcfb702fd41e47d4573766b9114da2fddbac20dcc4957/fonttools-4.57.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1d6bc9c23356908db712d282acb3eebd4ae5ec6d8b696aa40342b1d84f8e9e3", size = 2190799, upload_time = "2025-04-03T11:06:34.784Z" },
+    { url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605, upload_time = "2025-04-03T11:07:11.341Z" },
+]
+
+[[package]]
+name = "frozendict"
+version = "2.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload_time = "2024-10-13T12:15:32.449Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload_time = "2024-10-13T12:15:26.839Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload_time = "2024-10-13T12:15:28.16Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/8e/b6bf6a0de482d7d7d7a2aaac8fdc4a4d0bb24a809f5ddd422aa7060eb3d2/frozendict-2.4.6-py313-none-any.whl", hash = "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757", size = 16146, upload_time = "2024-10-13T12:15:29.495Z" },
+]
+
 [[package]]
 name = "frozenlist"
 version = "1.6.0"
@@ -475,6 +630,75 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" },
 ]
 
+[[package]]
+name = "kiwisolver"
+version = "1.4.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload_time = "2024-12-24T18:30:51.519Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload_time = "2024-12-24T18:29:16.85Z" },
+    { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload_time = "2024-12-24T18:29:19.146Z" },
+    { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload_time = "2024-12-24T18:29:20.096Z" },
+    { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload_time = "2024-12-24T18:29:22.843Z" },
+    { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload_time = "2024-12-24T18:29:24.463Z" },
+    { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload_time = "2024-12-24T18:29:25.776Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload_time = "2024-12-24T18:29:27.202Z" },
+    { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload_time = "2024-12-24T18:29:28.638Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload_time = "2024-12-24T18:29:30.368Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload_time = "2024-12-24T18:29:33.151Z" },
+    { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload_time = "2024-12-24T18:29:34.584Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload_time = "2024-12-24T18:29:36.138Z" },
+    { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload_time = "2024-12-24T18:29:39.991Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload_time = "2024-12-24T18:29:42.006Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload_time = "2024-12-24T18:29:44.38Z" },
+    { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload_time = "2024-12-24T18:29:45.368Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload_time = "2024-12-24T18:29:46.37Z" },
+    { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload_time = "2024-12-24T18:29:47.333Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload_time = "2024-12-24T18:29:49.636Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload_time = "2024-12-24T18:29:51.164Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload_time = "2024-12-24T18:29:52.594Z" },
+    { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload_time = "2024-12-24T18:29:53.941Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload_time = "2024-12-24T18:29:56.523Z" },
+    { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload_time = "2024-12-24T18:29:57.989Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload_time = "2024-12-24T18:29:59.393Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload_time = "2024-12-24T18:30:01.338Z" },
+    { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload_time = "2024-12-24T18:30:04.574Z" },
+    { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload_time = "2024-12-24T18:30:06.25Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload_time = "2024-12-24T18:30:07.535Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload_time = "2024-12-24T18:30:08.504Z" },
+    { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload_time = "2024-12-24T18:30:09.508Z" },
+    { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload_time = "2024-12-24T18:30:11.039Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload_time = "2024-12-24T18:30:14.886Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload_time = "2024-12-24T18:30:18.927Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload_time = "2024-12-24T18:30:22.102Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload_time = "2024-12-24T18:30:24.947Z" },
+    { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload_time = "2024-12-24T18:30:26.286Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload_time = "2024-12-24T18:30:28.86Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload_time = "2024-12-24T18:30:30.34Z" },
+    { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload_time = "2024-12-24T18:30:33.334Z" },
+    { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload_time = "2024-12-24T18:30:34.939Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload_time = "2024-12-24T18:30:37.281Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload_time = "2024-12-24T18:30:40.019Z" },
+]
+
+[[package]]
+name = "llvmlite"
+version = "0.44.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload_time = "2025-01-20T11:14:41.342Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload_time = "2025-01-20T11:13:32.57Z" },
+    { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload_time = "2025-01-20T11:13:38.744Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload_time = "2025-01-20T11:13:46.711Z" },
+    { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload_time = "2025-01-20T11:13:56.159Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload_time = "2025-01-20T11:14:02.442Z" },
+    { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload_time = "2025-01-20T11:14:09.035Z" },
+    { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090, upload_time = "2025-01-20T11:14:15.401Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904, upload_time = "2025-01-20T11:14:22.949Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245, upload_time = "2025-01-20T11:14:31.731Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193, upload_time = "2025-01-20T11:14:38.578Z" },
+]
+
 [[package]]
 name = "mako"
 version = "1.3.10"
@@ -525,6 +749,43 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload_time = "2024-10-18T15:21:42.784Z" },
 ]
 
+[[package]]
+name = "matplotlib"
+version = "3.10.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "contourpy" },
+    { name = "cycler" },
+    { name = "fonttools" },
+    { name = "kiwisolver" },
+    { name = "numpy" },
+    { name = "packaging" },
+    { name = "pillow" },
+    { name = "pyparsing" },
+    { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload_time = "2025-05-08T19:10:54.39Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload_time = "2025-05-08T19:10:07.602Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload_time = "2025-05-08T19:10:09.383Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload_time = "2025-05-08T19:10:11.958Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload_time = "2025-05-08T19:10:14.47Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload_time = "2025-05-08T19:10:16.569Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload_time = "2025-05-08T19:10:18.663Z" },
+    { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload_time = "2025-05-08T19:10:20.426Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload_time = "2025-05-08T19:10:22.569Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload_time = "2025-05-08T19:10:24.749Z" },
+    { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload_time = "2025-05-08T19:10:27.03Z" },
+    { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload_time = "2025-05-08T19:10:29.056Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload_time = "2025-05-08T19:10:31.221Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload_time = "2025-05-08T19:10:33.114Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload_time = "2025-05-08T19:10:35.337Z" },
+    { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload_time = "2025-05-08T19:10:37.611Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload_time = "2025-05-08T19:10:39.892Z" },
+    { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload_time = "2025-05-08T19:10:42.376Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload_time = "2025-05-08T19:10:44.551Z" },
+]
+
 [[package]]
 name = "multidict"
 version = "6.4.3"
@@ -585,6 +846,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400, upload_time = "2025-04-10T22:20:16.445Z" },
 ]
 
+[[package]]
+name = "multitasking"
+version = "0.0.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/75/345e196762fc51fb5b4e9504631972b1271a0cb2ba1ce2afe5b185c95b64/multitasking-0.0.11.tar.gz", hash = "sha256:4d6bc3cc65f9b2dca72fb5a787850a88dae8f620c2b36ae9b55248e51bcd6026", size = 8150, upload_time = "2022-06-28T08:40:46.278Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3e/8a/bb3160e76e844db9e69a413f055818969c8acade64e1a9ac5ce9dfdcf6c1/multitasking-0.0.11-py3-none-any.whl", hash = "sha256:1e5b37a5f8fc1e6cfaafd1a82b6b1cc6d2ed20037d3b89c25a84f499bd7b3dd4", size = 8533, upload_time = "2022-06-28T08:40:44.524Z" },
+]
+
 [[package]]
 name = "mypy"
 version = "1.15.0"
@@ -619,6 +889,28 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload_time = "2025-04-22T14:54:22.983Z" },
 ]
 
+[[package]]
+name = "numba"
+version = "0.61.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "llvmlite" },
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload_time = "2025-04-09T02:58:07.659Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload_time = "2025-04-09T02:57:51.857Z" },
+    { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287, upload_time = "2025-04-09T02:57:53.658Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928, upload_time = "2025-04-09T02:57:55.206Z" },
+    { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115, upload_time = "2025-04-09T02:57:56.818Z" },
+    { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929, upload_time = "2025-04-09T02:57:58.45Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload_time = "2025-04-09T02:57:59.96Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289, upload_time = "2025-04-09T02:58:01.435Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918, upload_time = "2025-04-09T02:58:02.933Z" },
+    { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056, upload_time = "2025-04-09T02:58:04.538Z" },
+    { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846, upload_time = "2025-04-09T02:58:06.125Z" },
+]
+
 [[package]]
 name = "numpy"
 version = "2.2.5"
@@ -657,6 +949,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/63/be/b85e4aa4bf42c6502851b971f1c326d583fcc68227385f92089cf50a7b45/numpy-2.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:d403c84991b5ad291d3809bace5e85f4bbf44a04bdc9a88ed2bb1807b3360bb8", size = 12750096, upload_time = "2025-04-19T22:47:00.147Z" },
 ]
 
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload_time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload_time = "2025-04-19T11:48:57.875Z" },
+]
+
 [[package]]
 name = "pamqp"
 version = "3.3.0"
@@ -722,6 +1023,62 @@ dependencies = [
 ]
 sdist = { url = "https://files.pythonhosted.org/packages/f7/0b/1666f0a185d4f08215f53cc088122a73c92421447b04028f0464fabe1ce6/pandas_ta-0.3.14b.tar.gz", hash = "sha256:0fa35aec831d2815ea30b871688a8d20a76b288a7be2d26cc00c35cd8c09a993", size = 115089, upload_time = "2021-07-28T20:51:17.456Z" }
 
+[[package]]
+name = "peewee"
+version = "3.18.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/ce/c2bb58d00cb12d19dea28d5a98d05a14350197a3d03eba60be9bae708bac/peewee-3.18.1.tar.gz", hash = "sha256:a76a694b3b3012ce22f00d51fd83e55bf80b595275a90ed62cd36eb45496cf1d", size = 3026130, upload_time = "2025-04-30T15:40:35.06Z" }
+
+[[package]]
+name = "pillow"
+version = "11.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload_time = "2025-04-12T17:50:03.289Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload_time = "2025-04-12T17:48:00.417Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload_time = "2025-04-12T17:48:02.391Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload_time = "2025-04-12T17:48:04.554Z" },
+    { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload_time = "2025-04-12T17:48:06.831Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload_time = "2025-04-12T17:48:09.229Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload_time = "2025-04-12T17:48:11.631Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload_time = "2025-04-12T17:48:13.592Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload_time = "2025-04-12T17:48:15.938Z" },
+    { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload_time = "2025-04-12T17:48:17.885Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload_time = "2025-04-12T17:48:19.655Z" },
+    { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload_time = "2025-04-12T17:48:21.991Z" },
+    { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload_time = "2025-04-12T17:48:23.915Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload_time = "2025-04-12T17:48:25.738Z" },
+    { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload_time = "2025-04-12T17:48:27.908Z" },
+    { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload_time = "2025-04-12T17:48:29.888Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload_time = "2025-04-12T17:48:31.874Z" },
+    { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload_time = "2025-04-12T17:48:34.422Z" },
+    { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload_time = "2025-04-12T17:48:37.641Z" },
+    { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload_time = "2025-04-12T17:48:39.652Z" },
+    { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload_time = "2025-04-12T17:48:41.765Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload_time = "2025-04-12T17:48:43.625Z" },
+    { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload_time = "2025-04-12T17:48:45.475Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload_time = "2025-04-12T17:48:47.866Z" },
+    { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload_time = "2025-04-12T17:48:50.189Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload_time = "2025-04-12T17:48:52.346Z" },
+    { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload_time = "2025-04-12T17:48:54.403Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload_time = "2025-04-12T17:48:56.383Z" },
+    { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload_time = "2025-04-12T17:48:58.782Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload_time = "2025-04-12T17:49:00.709Z" },
+    { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload_time = "2025-04-12T17:49:02.946Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload_time = "2025-04-12T17:49:04.889Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload_time = "2025-04-12T17:49:06.635Z" },
+    { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload_time = "2025-04-12T17:49:08.399Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload_time = "2025-05-07T22:47:42.121Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload_time = "2025-05-07T22:47:40.376Z" },
+]
+
 [[package]]
 name = "propcache"
 version = "0.3.1"
@@ -779,6 +1136,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload_time = "2025-03-26T03:06:10.5Z" },
 ]
 
+[[package]]
+name = "protobuf"
+version = "5.29.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/17/7d/b9dca7365f0e2c4fa7c193ff795427cfa6290147e5185ab11ece280a18e7/protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99", size = 424902, upload_time = "2025-03-19T21:23:24.25Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9a/b2/043a1a1a20edd134563699b0e91862726a0dc9146c090743b6c44d798e75/protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7", size = 422709, upload_time = "2025-03-19T21:23:08.293Z" },
+    { url = "https://files.pythonhosted.org/packages/79/fc/2474b59570daa818de6124c0a15741ee3e5d6302e9d6ce0bdfd12e98119f/protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d", size = 434506, upload_time = "2025-03-19T21:23:11.253Z" },
+    { url = "https://files.pythonhosted.org/packages/46/de/7c126bbb06aa0f8a7b38aaf8bd746c514d70e6a2a3f6dd460b3b7aad7aae/protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0", size = 417826, upload_time = "2025-03-19T21:23:13.132Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/b5/bade14ae31ba871a139aa45e7a8183d869efe87c34a4850c87b936963261/protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e", size = 319574, upload_time = "2025-03-19T21:23:14.531Z" },
+    { url = "https://files.pythonhosted.org/packages/46/88/b01ed2291aae68b708f7d334288ad5fb3e7aa769a9c309c91a0d55cb91b0/protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922", size = 319672, upload_time = "2025-03-19T21:23:15.839Z" },
+    { url = "https://files.pythonhosted.org/packages/12/fb/a586e0c973c95502e054ac5f81f88394f24ccc7982dac19c515acd9e2c93/protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862", size = 172551, upload_time = "2025-03-19T21:23:22.682Z" },
+]
+
 [[package]]
 name = "psycopg"
 version = "3.2.6"
@@ -858,6 +1229,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810, upload_time = "2025-04-02T09:48:17.97Z" },
 ]
 
+[[package]]
+name = "pyparsing"
+version = "3.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload_time = "2025-03-25T05:01:28.114Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload_time = "2025-03-25T05:01:24.908Z" },
+]
+
 [[package]]
 name = "python-dateutil"
 version = "2.9.0.post0"
@@ -888,6 +1268,21 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502, upload_time = "2024-12-06T09:50:39.656Z" },
 ]
 
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "certifi" },
+    { name = "charset-normalizer" },
+    { name = "idna" },
+    { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" },
+]
+
 [[package]]
 name = "ruff"
 version = "0.11.7"
@@ -913,6 +1308,44 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/68/ca/69d7c7752bce162d1516e5592b1cc6b6668e9328c0d270609ddbeeadd7cf/ruff-0.11.7-py3-none-win_arm64.whl", hash = "sha256:778c1e5d6f9e91034142dfd06110534ca13220bfaad5c3735f6cb844654f6177", size = 10677936, upload_time = "2025-04-24T18:49:34.392Z" },
 ]
 
+[[package]]
+name = "scipy"
+version = "1.15.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload_time = "2025-05-08T16:13:05.955Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload_time = "2025-05-08T16:06:06.471Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload_time = "2025-05-08T16:06:11.686Z" },
+    { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload_time = "2025-05-08T16:06:15.97Z" },
+    { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload_time = "2025-05-08T16:06:20.394Z" },
+    { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload_time = "2025-05-08T16:06:26.159Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload_time = "2025-05-08T16:06:32.778Z" },
+    { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload_time = "2025-05-08T16:06:39.249Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload_time = "2025-05-08T16:06:45.729Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload_time = "2025-05-08T16:06:52.623Z" },
+    { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload_time = "2025-05-08T16:06:58.696Z" },
+    { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload_time = "2025-05-08T16:07:04.209Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload_time = "2025-05-08T16:07:08.998Z" },
+    { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload_time = "2025-05-08T16:07:14.091Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload_time = "2025-05-08T16:07:19.427Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload_time = "2025-05-08T16:07:25.712Z" },
+    { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload_time = "2025-05-08T16:07:31.468Z" },
+    { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload_time = "2025-05-08T16:07:38.002Z" },
+    { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload_time = "2025-05-08T16:08:33.671Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload_time = "2025-05-08T16:07:44.039Z" },
+    { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload_time = "2025-05-08T16:07:49.891Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload_time = "2025-05-08T16:07:54.121Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload_time = "2025-05-08T16:07:58.506Z" },
+    { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload_time = "2025-05-08T16:08:03.929Z" },
+    { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload_time = "2025-05-08T16:08:09.558Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload_time = "2025-05-08T16:08:15.34Z" },
+    { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload_time = "2025-05-08T16:08:21.513Z" },
+    { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload_time = "2025-05-08T16:08:27.627Z" },
+]
+
 [[package]]
 name = "setuptools"
 version = "80.1.0"
@@ -940,6 +1373,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload_time = "2024-02-25T23:20:01.196Z" },
 ]
 
+[[package]]
+name = "soupsieve"
+version = "2.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload_time = "2025-04-20T18:50:08.518Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload_time = "2025-04-20T18:50:07.196Z" },
+]
+
 [[package]]
 name = "sqlalchemy"
 version = "2.0.40"
@@ -1005,17 +1447,21 @@ dependencies = [
     { name = "fastapi" },
     { name = "faststream" },
     { name = "httpx" },
+    { name = "matplotlib" },
     { name = "mypy" },
+    { name = "numba" },
     { name = "pandas" },
     { name = "pandas-stubs" },
     { name = "pandas-ta" },
     { name = "psycopg" },
     { name = "redis" },
     { name = "ruff" },
+    { name = "scipy" },
     { name = "sqlalchemy" },
     { name = "ta-lib" },
     { name = "uvicorn" },
     { name = "websockets" },
+    { name = "yfinance" },
 ]
 
 [package.metadata]
@@ -1029,17 +1475,21 @@ requires-dist = [
     { name = "fastapi", specifier = ">=0.115.12" },
     { name = "faststream", specifier = ">=0.5.39" },
     { name = "httpx", specifier = ">=0.28.1" },
+    { name = "matplotlib", specifier = ">=3.10.3" },
     { name = "mypy", specifier = ">=1.15.0" },
+    { name = "numba", specifier = ">=0.61.2" },
     { name = "pandas", specifier = ">=2.2.3" },
     { name = "pandas-stubs", specifier = ">=2.2.3.250308" },
     { name = "pandas-ta", specifier = ">=0.3.14b0" },
     { name = "psycopg", specifier = ">=3.2.6" },
     { name = "redis", specifier = ">=5.2.1" },
     { name = "ruff", specifier = ">=0.11.7" },
+    { name = "scipy", specifier = ">=1.15.3" },
     { name = "sqlalchemy", specifier = ">=2.0.40" },
     { name = "ta-lib", specifier = ">=0.6.3" },
     { name = "uvicorn", specifier = ">=0.34.2" },
     { name = "websockets", specifier = ">=15.0.1" },
+    { name = "yfinance", specifier = ">=0.2.59" },
 ]
 
 [[package]]
@@ -1081,6 +1531,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload_time = "2025-03-23T13:54:41.845Z" },
 ]
 
+[[package]]
+name = "urllib3"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload_time = "2025-04-10T15:23:39.232Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload_time = "2025-04-10T15:23:37.377Z" },
+]
+
 [[package]]
 name = "uvicorn"
 version = "0.34.2"
@@ -1189,3 +1648,26 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/3f/93/f73b61353b2a699d489e782c3f5998b59f974ec3156a2050a52dfd7e8946/yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", size = 101093, upload_time = "2025-04-17T00:44:27.418Z" },
     { url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124, upload_time = "2025-04-17T00:45:12.199Z" },
 ]
+
+[[package]]
+name = "yfinance"
+version = "0.2.59"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "beautifulsoup4" },
+    { name = "curl-cffi" },
+    { name = "frozendict" },
+    { name = "multitasking" },
+    { name = "numpy" },
+    { name = "pandas" },
+    { name = "peewee" },
+    { name = "platformdirs" },
+    { name = "protobuf" },
+    { name = "pytz" },
+    { name = "requests" },
+    { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/91/f2/776756439ce8de7b4e6c6df7c72340c6e6d6efe4d413011591d4432ef407/yfinance-0.2.59.tar.gz", hash = "sha256:632a6dbf6f912a9ca27fd4677903bec779a527bf29a5f9889af845d73c5ab7f3", size = 126907, upload_time = "2025-05-07T08:47:16.497Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b4/53/c65b103b8ad6c3c0bc33d10493eb8721f2d7417886f1d814ef58f96482fd/yfinance-0.2.59-py2.py3-none-any.whl", hash = "sha256:9fdb762ee1be969af1cf592c7245952fd88790b8eebb71311153cbca39fee01c", size = 117376, upload_time = "2025-05-07T08:47:14.909Z" },
+]


Reviews:
user: login: Pavel14701
body: Ok
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T08:04:13Z
user: login: sourcery-ai[bot]
body: Привет, @Pavel14701 - я просмотрел ваши изменения и обнаружил некоторые проблемы, которые необходимо решить.

**Блокирующие проблемы**:
- Названия столбцов сигналов в get_last_signal не совпадают с названиями в create_signals. ([ссылка](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-443d2d8199eec9e6c0c9b9e5f8d88d877c61d2cb2a55e1ed3c9ef1d62877ba02R123))

- Рассмотрите возможность стандартизации названий столбцов сигналов (например, «buy_signals», «sell_signals», «Buy Signals», «Sell Signals») во всех реализациях индикаторов для обеспечения согласованности.
- Некоторые классы индикаторов (например, StochRSI, AccelerationBands) используют разные имена методов для возврата последнего сигнала (например, get_last_signal, check_last_signal); согласование этих имен методов улучшит единообразие API.

<details>
<summary>Вот что я смотрел во время обзора</summary>

- 🔴 **Общие проблемы**: 1 блокирующая проблема, 3 другие проблемы
- 🟢 **Безопасность**: все выглядит хорошо
- 🟢 **Тестирование**: все выглядит хорошо
- 🟡 **Сложность**: обнаружена 1 проблема
- 🟢 **Документация**: все выглядит хорошо
</details>

***

<details>
<summary>Sourcery бесплатен для открытого исходного кода — если вам нравятся наши обзоры, пожалуйста, поделитесь ими ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Помогите мне быть полезнее! Пожалуйста, нажмите 👍 или 👎 на каждом комментарии, и я буду использовать отзывы для улучшения ваших обзоров.
</sub>

<details>
<summary>Original comment in English</summary>

Hey @Pavel14701 - I've reviewed your changes and found some issues that need to be addressed.

**Blocking issues**:
- Signal column names in get_last_signal do not match those in create_signals. ([link](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-443d2d8199eec9e6c0c9b9e5f8d88d877c61d2cb2a55e1ed3c9ef1d62877ba02R123))

- Consider standardizing the naming of signal columns (e.g., 'buy_signals', 'sell_signals', 'Buy Signals', 'Sell Signals') across all indicator implementations for consistency.
- Some indicator classes (e.g., StochRSI, AccelerationBands) use different method names for returning the last signal (e.g., get_last_signal, check_last_signal); aligning these method names would improve API uniformity.

<details>
<summary>Here's what I looked at during the review</summary>

- 🔴 **General issues**: 1 blocking issue, 3 other issues
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T08:09:31Z
user: login: sourcery-ai[bot]
body: Привет, @Pavel14701 - я проверил ваши изменения и обнаружил некоторые проблемы, которые необходимо решить.

**Блокирующие проблемы**:
- Названия столбцов сигналов в get_last_signal не совпадают с названиями в create_signals. ([link](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-443d2d8199eec9e6c0c9b9e5f8d88d877c61d2cb2a55e1ed3c9ef1d62877ba02R123))

<details>
<summary>Вот что я смотрел во время проверки</summary>

- 🔴 **Общие проблемы**: 1 блокирующая проблема, 4 другие проблемы
- 🟢 **Безопасность**: все выглядит хорошо
- 🟢 **Тестирование**: все выглядит хорошо
- 🟡 **Сложность**: обнаружена 1 проблема
- 🟢 **Документация**: все выглядит хорошо
</details>

***

<details>
<summary>Sourcery бесплатен для открытого исходного кода - если вам нравятся наши обзоры, пожалуйста, поделитесь ими ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Помогите мне быть более полезным! Пожалуйста, нажмите 👍 или 👎 на каждом комментарии, и я буду использовать обратную связь для улучшения ваших обзоров.
</sub>

<details>
<summary>Original comment in English</summary>

Hey @Pavel14701 - I've reviewed your changes and found some issues that need to be addressed.

**Blocking issues**:
- Signal column names in get_last_signal do not match those in create_signals. ([link](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-443d2d8199eec9e6c0c9b9e5f8d88d877c61d2cb2a55e1ed3c9ef1d62877ba02R123))

<details>
<summary>Here's what I looked at during the review</summary>

- 🔴 **General issues**: 1 blocking issue, 4 other issues
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T08:26:05Z
user: login: Pavel14701
body: Ok
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T08:26:23Z
user: login: Pavel14701
body: Ok
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T08:36:59Z
user: login: Pavel14701
body: Ok
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-11T08:39:19Z
user: login: sourcery-ai[bot]
body: Привет, @Pavel14701 - я просмотрел ваши изменения и обнаружил некоторые проблемы, которые необходимо решить.

**Блокирующие проблемы**:
- Имена столбцов сигналов в get_last_signal не совпадают с именами в create_signals. ([link](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-443d2d8199eec9e6c0c9b9e5f8d88d877c61d2cb2a55e1ed3c9ef1d62877ba02R123))

- Рассмотрите возможность стандартизации имен свойств в PriceDataFrame (например, open_price vs open_prices), чтобы избежать путаницы и обеспечить согласованность во всей кодовой базе.
- Некоторые новые классы индикаторов (например, StochRSI, AccelerationBands) используют несогласованные соглашения об именах сигналов (например, «Buy Signals» vs «buy_signals»); согласование этих соглашений улучшит интеграцию с последующими этапами.

<details>
<summary>Вот что я смотрел во время обзора</summary>

- 🔴 **Общие проблемы**: 1 блокирующая проблема, 2 другие проблемы
- 🟢 **Безопасность**: все выглядит хорошо
- 🟢 **Тестирование**: все выглядит хорошо
- 🟡 **Сложность**: обнаружена 1 проблема
- 🟢 **Документация**: все выглядит хорошо
</details>

***

<details>
<summary>Sourcery бесплатен для открытого исходного кода — если вам нравятся наши обзоры, пожалуйста, поделитесь ими ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Помогите мне быть более полезным! Пожалуйста, нажмите 👍 или 👎 на каждом комментарии, и я использую отзыв для улучшения ваших обзоров.
</sub>

<details>
<summary>Original comment in English</summary>

Hey @Pavel14701 - I've reviewed your changes and found some issues that need to be addressed.

**Blocking issues**:
- Signal column names in get_last_signal do not match those in create_signals. ([link](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-443d2d8199eec9e6c0c9b9e5f8d88d877c61d2cb2a55e1ed3c9ef1d62877ba02R123))

- Consider standardizing property names in PriceDataFrame (e.g., open_price vs open_prices) to avoid confusion and ensure consistency across the codebase.
- Some new indicator classes (e.g., StochRSI, AccelerationBands) use inconsistent signal naming conventions (e.g., 'Buy Signals' vs 'buy_signals'); aligning these would improve downstream integration.

<details>
<summary>Here's what I looked at during the review</summary>

- 🔴 **General issues**: 1 blocking issue, 2 other issues
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T08:43:13Z
user: login: sourcery-ai[bot]
body: Привет, @Pavel14701 - я проверил ваши изменения и обнаружил некоторые проблемы, которые необходимо решить.

**Блокирующие проблемы**:
- Логика сигнала кажется инвертированной для условий покупки/продажи. ([link](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-f0986930209bf7b2dc077e81285308c329705226e2e6ccb82ea6ca4d25172511R78))

- Рассмотрите возможность пересмотра переименований свойств PriceDataFrame (например, close_price → close_prices), чтобы убедиться, что весь нисходящий код и логика индикаторов обновлены для обеспечения согласованности.
- Некоторые новые классы индикаторов (например, AccelerationBands, OrderBlockDetector) вызывают или возвращают ошибки, если вычисления не удаются; рассмотрите возможность стандартизации обработки ошибок или возвращаемых значений для неудачных вычислений во всех индикаторах.

<details>
<summary>Вот что я смотрел во время обзора</summary>

- 🔴 **Общие проблемы**: 1 блокирующая проблема, 8 других проблем
- 🟢 **Безопасность**: все выглядит хорошо
- 🟢 **Тестирование**: все выглядит хорошо
- 🟡 **Сложность**: 1 проблема найдена
- 🟢 **Документация**: все выглядит хорошо
</details>

***

<details>
<summary>Sourcery бесплатен для открытого исходного кода - если вам нравятся наши обзоры, пожалуйста, поделитесь ими ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Помогите мне быть более полезным! Пожалуйста, нажмите 👍 или 👎 на каждом комментарии, и я буду использовать обратную связь для улучшения ваших обзоров.
</sub>

<details>
<summary>Original comment in English</summary>

Hey @Pavel14701 - I've reviewed your changes and found some issues that need to be addressed.

**Blocking issues**:
- Signal logic appears inverted for buy/sell conditions. ([link](https://github.com/Pavel14701/trade_project_bot/pull/18/files#diff-f0986930209bf7b2dc077e81285308c329705226e2e6ccb82ea6ca4d25172511R78))

- Consider reviewing the new PriceDataFrame property renamings (e.g., close_price → close_prices) to ensure all downstream code and indicator logic are updated for consistency.
- Some new indicator classes (e.g., AccelerationBands, OrderBlockDetector) raise or return errors if calculations fail; consider standardizing error handling or return values for failed computations across all indicators.

<details>
<summary>Here's what I looked at during the review</summary>

- 🔴 **General issues**: 1 blocking issue, 8 other issues
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>

</details>
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-11T08:48:39Z

Review comments:
diff_hunk: @@ -0,0 +1,225 @@
+import pandas as pd
+import numpy as np
+from numba import njit
+from numpy.typing import NDArray
+import pandas_ta as ta
+
+from strategies.src.domain.entities import AvslConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class AVSL:
path: strategies/src/infrastructure/indicators/avsl.py
user: login: sourcery-ai[bot]
body: **issue (complexity):** Рассмотрите возможность извлечения вычислений технических характеристик и разбиения вспомогательной логики на более мелкие функции для уточнения потока.

Вот несколько небольших рефакторингов, которые сделают поток намного понятнее, не меняя при этом никакого поведения. По сути, мы

 1. выносим все ваши необработанные вычисления технических характеристик из `calculate_avsl` в свой собственный помощник
 2. разбиваем `_price_fun` на два крошечных шага numpy (`compute_len_v`, `compute_vpcc`)
 3. оставляем цикл JIT точно таким, какой он есть

–––

### 1) Извлеките все VWMA/VPC/VPR/VM/VPCI в свой собственный помощник

До (внутри `calculate_avsl`):
```python
# … 15 строк вызовов pd-ta, np.where и т. д. …
vw_ma_fast = ta.vwma(...)
vw_ma_slow = ta.vwma(...)
vpc        = vw_ma_slow - ta.sma(...)
vpr        = vw_ma_fast / ta.sma(...)
vm         = ta.sma(vol_fast) / ta.sma(vol_slow)
vpci       = vpc * vpr * vm
```

После:
```python
def _compute_base_series(self, data, cfg):
    fast = ta.vwma(close=data.close_prices, volume=data.volumes, length=cfg.length_fast)
    slow = ta.vwma(close=data.close_prices, volume=data.volumes, length=cfg.length_slow)
    vpc  = slow - ta.sma(close=data.close_prices, length=cfg.length_slow, talib=True)
    vpr  = fast / ta.sma(close=data.close_prices, length=cfg.length_fast, talib=True)
    vm   = (ta.sma(close=data.volumes, length=cfg.length_fast, talib=True)
            / ta.sma(close=data.volumes, length=cfg.length_slow, talib=True))
    return fast, slow, vpc, vpr, vm, vpc * vpr * vm
```

И теперь `calculate_avsl` становится:
```python
def calculate_avsl(self, data, config):
    vw_f, vw_s, vpc, vpr, vm, vpci = self._compute_base_series(data, config)
    price_v = self._price_fun(data, vpc, vpr, vpci)
    deviation = config.stand_div * vpci * vm
    afsl = ta.sma(close=data.low_prices - price_v + deviation,
                  length=config.length_slow, talib=True)
    return pd.DataFrame({"avsl": afsl}, index=data.index)
```

### 2) Разбейте `_price_fun` на два крошечных помощника numpy

До (внутри `_price_fun`):
```python
lenV = np.where(... вложенный np.where ...)
VPCc = np.where(... вложенный np.where ...)
return _compute_price_v(low_np, vpr_np, lenV, VPCc)
```

После:
```python
def compute_len_v(vpc, vpci):
    # та же логика np.where, только с именами
    out = np.where(np.isnan(vpci), 1,
          np.where(vpc < 0,
                   np.round(np.abs(vpci - 3)).astype(np.int32),
                   np.round(vpci + 3).astype(np.int32)))
    return out

def compute_vpcc(vpc):
    # та же логика для VPCc
    return np.where((vpc > -1) & (vpc < 0), -1.0,
           np.where((vpc >= 0) & (vpc < 1), 1.0, vpc))

def _price_fun(self, data, vpc, vpr, vpci):
    low_np, vpc_np, vpr_np, vpci_np = (s.to_numpy() for s in (data.low_prices, vpc, vpr, vpci))
    lenV = compute_len_v(vpc_np, vpci_np)
    VPCc = compute_vpcc(vpc_np)
    return _compute_price_v(low_np, vpr_np, lenV, VPCc)
```

Таким образом, каждый фрагмент состоит из двух строк, а «большие» методы просто управляют ими. Цикл JIT остается нетронутым (и по-прежнему делает только одно дело).

<details>
<summary>Original comment in English</summary>

**issue (complexity):** Consider extracting technical feature computations and breaking up helper logic into smaller functions to clarify the flow.

Here are a few small refactorings that will make the flow much clearer without changing any behavior. In essence we

 1. pull all of your raw technical feature-computations out of `calculate_avsl` into its own helper  
 2. break `_price_fun` into two tiny numpy steps (`compute_len_v`, `compute_vpcc`)  
 3. keep the JIT loop exactly as-is

–––  

### 1) Extract all VWMA/VPC/VPR/VM/VPCI into its own helper  

Before (inside `calculate_avsl`):  
```python
# … 15 lines of pd-ta calls, np.where, etc. …
vw_ma_fast = ta.vwma(...)
vw_ma_slow = ta.vwma(...)
vpc        = vw_ma_slow - ta.sma(...)
vpr        = vw_ma_fast / ta.sma(...)
vm         = ta.sma(vol_fast) / ta.sma(vol_slow)
vpci       = vpc * vpr * vm
```

After:
```python
def _compute_base_series(self, data, cfg):
    fast = ta.vwma(close=data.close_prices, volume=data.volumes, length=cfg.length_fast)
    slow = ta.vwma(close=data.close_prices, volume=data.volumes, length=cfg.length_slow)
    vpc  = slow - ta.sma(close=data.close_prices, length=cfg.length_slow, talib=True)
    vpr  = fast / ta.sma(close=data.close_prices, length=cfg.length_fast, talib=True)
    vm   = (ta.sma(close=data.volumes, length=cfg.length_fast, talib=True)
            / ta.sma(close=data.volumes, length=cfg.length_slow, talib=True))
    return fast, slow, vpc, vpr, vm, vpc * vpr * vm
```

And now `calculate_avsl` becomes:
```python
def calculate_avsl(self, data, config):
    vw_f, vw_s, vpc, vpr, vm, vpci = self._compute_base_series(data, config)
    price_v = self._price_fun(data, vpc, vpr, vpci)
    deviation = config.stand_div * vpci * vm
    afsl = ta.sma(close=data.low_prices - price_v + deviation,
                  length=config.length_slow, talib=True)
    return pd.DataFrame({"avsl": afsl}, index=data.index)
```

### 2) Break `_price_fun` into two tiny numpy helpers  

Before (inside `_price_fun`):  
```python
lenV = np.where(... nested np.where ...)
VPCc = np.where(... nested np.where ...)
return _compute_price_v(low_np, vpr_np, lenV, VPCc)
```

After:
```python
def compute_len_v(vpc, vpci):
    # same np.where logic, just named
    out = np.where(np.isnan(vpci), 1,
          np.where(vpc < 0,
                   np.round(np.abs(vpci - 3)).astype(np.int32),
                   np.round(vpci + 3).astype(np.int32)))
    return out

def compute_vpcc(vpc):
    # same logic for VPCc
    return np.where((vpc > -1) & (vpc < 0), -1.0,
           np.where((vpc >= 0) & (vpc < 1), 1.0, vpc))

def _price_fun(self, data, vpc, vpr, vpci):
    low_np, vpc_np, vpr_np, vpci_np = (s.to_numpy() for s in (data.low_prices, vpc, vpr, vpci))
    lenV = compute_len_v(vpc_np, vpci_np)
    VPCc = compute_vpcc(vpc_np)
    return _compute_price_v(low_np, vpr_np, lenV, VPCc)
```

This way each piece is a two-liner, and the “big” methods just orchestrate them. The JIT loop stays untouched (and still just does one thing).

</details>
created_at: 2025-05-11T08:09:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,225 @@
+import pandas as pd
+import numpy as np
+from numba import njit
+from numpy.typing import NDArray
+import pandas_ta as ta
+
+from strategies.src.domain.entities import AvslConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class AVSL:
+    """
+    Adaptive Volume-Weighted Support Level 
+      (AVSL) Indicator Implementation.
+
+    The AVSL indicator calculates **dynamic 
+      support and resistance levels** using volume-weighted 
+      moving averages and volatility metrics.
+
+    Formula:
+    1. Compute VWMA (**Volume-Weighted Moving Average**) 
+      over fast and slow periods.
+    2. Calculate **Volume Price Confirmation (VPC)** to 
+      measure price deviations.
+    3. Compute **Volume Price Ratio (VPR)** by dividing 
+      fast VWMA by a simple moving average.
+    4. Determine **Volume Momentum (VM)** by comparing 
+      fast and slow volume trends.
+    5. Combine VPC, VPR, and VM into **VPCI** for 
+      trend confirmation.
+    6. Apply a **custom price function** to 
+      adjust support/resistance levels.
+    7. Use a smoothed moving average (**SMA**) 
+      to finalize AVSL levels.
+
+    Usage:
+    - Identifies **strong support zones** based 
+      on price and volume dynamics.
+    - Provides adaptive thresholds for **trend 
+      validation and risk management**.
+    """
+
+    def calculate_avsl(
+        self, 
+        data: PriceDataFrame, 
+        config: AvslConfigDM
+    ) -> pd.DataFrame:
+        """
+        Computes the AVSL (Adaptive Support Level) values.
+        Args:
+            data (PriceDataFrame): Market price dataset containing 
+              high, low, close prices and volume.
+            config (AvslConfigDM): AVSL configuration parameters.
+        Returns:
+            pd.DataFrame: A DataFrame containing AVSL 
+              values indexed by date.
+        """
+        # Compute VWMA (Volume-Weighted Moving Average)
+        #  for fast and slow periods
+        vw_ma_fast: pd.Series = ta.vwma(
+            close=data.close_prices, 
+            volume=data.volumes, 
+            length=config.length_fast
+        )
+        vw_ma_slow: pd.Series = ta.vwma(
+            close=data.close_prices, 
+            volume=data.volumes,
+            length=config.length_slow
+        )
+        # Compute Volume Price Confirmation (VPC)
+        vpc: pd.Series = vw_ma_slow - ta.sma(
+            close=data.close_prices, 
+            length=config.length_slow,
+            talib=True
+        )
+        # Compute Volume Price Ratio (VPR)
+        vpr: pd.Series = vw_ma_fast / ta.sma(
+            close=data.close_prices, 
+            length=config.length_fast, 
+            talib=True
+        )
+        # Compute Volume Momentum (VM)
+        vm: pd.Series = ta.sma(
+            close=data.volumes, 
+            length=config.length_fast, 
+            talib=True
+        ) / ta.sma(
+            close=data.volumes, 
+            length=config.length_slow,
+            talib=True
+        )
+        # Compute Volume Price Confirmation Index (VPCI)
+        vpci: pd.Series = vpc * vpr * vm
+        # Compute custom price adjustment function
+        PriceV = self._price_fun(data, vpc, vpr, vpci)
+        # Compute AVSL deviation factor
+        deviation = config.stand_div * vpci * vm
+        # Compute final AVSL level using SMA
+        avsl = ta.sma(
+            close=data.low_prices - PriceV + deviation,
+            length=config.length_slow, 
+            talib=True
+        )
+        return pd.DataFrame({"avsl": avsl}, index=data.index)
+
+
+    def _price_fun(
+        self,
+        data: PriceDataFrame,
+        vpc: pd.Series,
+        vpr: pd.Series,
+        vpci: pd.Series,
+    ) -> NDArray:
+        """
+        Optimized price adjustment function using 
+          vectorized calculations and a 
+          Numba-jitted helper.
+
+        Args:
+            data (PriceDataFrame): Market price dataset.
+            vpc (pd.Series): Volume Price Confirmation values.
+            vpr (pd.Series): Volume Price Ratio values.
+            vpci (pd.Series): VPCI values.
+
+        Returns:
+            np.ndarray: Adjusted price levels based 
+              on volume-price interaction.
+        """
+        # Convert pandas Series to NumPy arrays for faster processing
+        low_np = data.low_prices.to_numpy()
+        vpc_np = vpc.to_numpy()
+        vpr_np = vpr.to_numpy()
+        vpci_np = vpci.to_numpy()
+        # Vectorized calculation of lenV
+        lenV = np.where(
+            np.isnan(vpci_np), 1,  # Prevent division by zero
+            np.where(
+                vpc_np < 0,
+                np.round(np.abs(vpci_np - 3)).astype(np.int32),
+                np.round(vpci_np + 3).astype(np.int32),
+            )
+        )
+        # Vectorized calculation of VPCc
+        VPCc = np.where(
+            (vpc_np > -1) & (vpc_np < 0),
+            -1.0,
+            np.where(
+                (
+                    vpc_np >= 0
+                ) & (
+                    vpc_np < 1
+                ), 1.0, vpc_np
+            ),
+        )
+        # Call the optimized JIT helper function
+        return _compute_price_v(
+            low_np, vpr_np, lenV, VPCc
+        )
+
+    def get_last_avsl_signal(
+            self, 
+            data: PriceDataFrame, 
+            config: AvslConfigDM
+        ) -> float | None:
+        """
+        Retrieves the AVSL value on the last bar.
+        Args:
+            data (PriceDataFrame): Market data with price information.
+            config (AvslConfigDM): Configuration settings for AVSL.
+        Returns:
+            float | None: AVSL value of the last bar if available, otherwise None.
+        """
+        # Compute AVSL values
+        avsl_df = self.calculate_avsl(data, config)
+        # Ensure AVSL is not empty before retrieving the last value
+        if avsl_df.empty or pd.isna(avsl_df["avsl"].iloc[-1]):
path: strategies/src/infrastructure/indicators/avsl.py
user: login: sourcery-ai[bot]
body: **suggestion (bug_risk):** Использование pd.isna для скалярного значения корректно, но может быть более надежным.

Добавьте проверку, содержит ли DataFrame столбец 'avsl', прежде чем обращаться к его последнему значению, чтобы избежать KeyError, если столбец отсутствует.

```suggestion
        if avsl_df.empty or "avsl" not in avsl_df.columns or pd.isna(avsl_df["avsl"].iloc[-1]):
```

<details>
<summary>Original comment in English</summary>

**suggestion (bug_risk):** Using pd.isna on a scalar value is correct, but could be more robust.

Add a check that the DataFrame contains an 'avsl' column before accessing its last value to avoid KeyError if the column is missing.

```suggestion
        if avsl_df.empty or "avsl" not in avsl_df.columns or pd.isna(avsl_df["avsl"].iloc[-1]):
```

</details>
created_at: 2025-05-11T08:26:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,27 +123,39 @@ def create_signals(self, ohlc: pd.DataFrame) -> pd.DataFrame:
                     ohlc['macd_line'] < ohlc['macd_signal']
                 ),
             ],
-            [1, -1],
+            # 1 → Buy, -1 → Sell
+            [1, -1],  
             default=0
         )
+        # Identify histogram crossing zero
         ohlc['histogram_cross_zero'] = np.select(
             [
                 (ohlc['histogram'].shift(1) < 0) & (ohlc['histogram'] > 0),
                 (ohlc['histogram'].shift(1) > 0) & (ohlc['histogram'] < 0),
             ],
-            [1, -1],
+            # 1 → Uptrend confirmation, -1 → Downtrend confirmation
+            [1, -1],  
             default=0
         )
         return ohlc
 
-    def get_last_signal(self, ohlc: pd.DataFrame) -> str:
-        """Определяет последний сигнал MACD (без учета гистограммы)."""
+    def get_last_signal(self, ohlc: pd.DataFrame) -> str | None:
+        """
+        Retrieves the last MACD crossover signal.
+        Args:
+            ohlc (pd.DataFrame): DataFrame containing 
+              RSI and MACD values.
+        Returns:
+            str | None: "buy" if MACD crossed above, "sell" if 
+              MACD crossed below, or None if no signal.
+        """
+        # Ensure data is available
         if ohlc.empty:
-            return "None"
+            return None
path: strategies/src/infrastructure/indicators/rsi_clouds.py
user: login: sourcery-ai[bot]
body: **question (bug_risk):** Возвращаемое значение для отсутствующего сигнала изменено со строки 'None' на None.

Убедитесь, что все вызывающие объекты обрабатывают возвращаемое значение None вместо строки 'None'.

<details>
<summary>Original comment in English</summary>

**question (bug_risk):** Return value for no signal changed from string 'None' to None.

Verify that all callers handle a None return value instead of the string 'None'.

</details>
created_at: 2025-05-11T08:26:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,5 @@
 from dataclasses import dataclass
+from typing import Any
 
 
 @dataclass(slots=True, frozen=True)
path: strategies/src/domain/entities.py
user: login: sourcery-ai[bot]
body: **issue (complexity):** Рассмотрите возможность разделения каждого класса данных конфигурации индикатора на свой собственный модуль внутри подпакета configs, чтобы entities.py оставался сфокусированным и поддерживаемым.

Вот один из способов уменьшить гигантский `entities.py`, сохранив при этом всю функциональность:

1) Создайте новую папку, например:
   ```
   domain/
     entities.py
     configs/
       __init__.py
       rsi_configs.py
       adx_configs.py
       …
   ```

2) Переместите каждый `@dataclass` в свой собственный модуль.
   **domain/configs/adc_configs.py**
   ```python
   # keep only ADX in here
   from dataclasses import dataclass
   from typing import Any

   @dataclass(slots=True, frozen=True)
   class AdxConfigDM:
       length:      int | None = None
       lensig:      int | None = None
       mamode:      str | None = None
       scalar:      float | int | None = None
       drift:       int | None = None
       offset:      int | None = None
   ```

   **domain/configs/rsi_configs.py**
   ```python
   from dataclasses import dataclass

   @dataclass(slots=True, frozen=True)
   class RsiCloudsConfigDM:
       rsi_length:  int | None = None
       macd_fast:   int | None = None
       macd_slow:   int | None = None
       macd_signal: int | None = None
       scalar:      float | None = None
       drift:       int | None = None
       offset:      int | None = None
       talib:       bool      = True
   ```

   Повторите для каждой из остальных 10+ конфигураций.

3) В вашем исходном `entities.py` замените все эти классы импортом:
   ```python
   from dataclasses import dataclass
   from typing import Any

   from .configs.rsi_configs import RsiCloudsConfigDM
   from .configs.adx_configs import AdxConfigDM
   # …other imports

   @dataclass(slots=True, frozen=True)
   class CreateTableDM:
       instId:    str
       bar:       str
       partition: str | None = None
   # no more indicator-configs here
   ```

4) (Необязательно) В `domain/configs/__init__.py` повторно экспортируйте, если хотите:
   ```python
   from .rsi_configs import RsiCloudsConfigDM
   from .adx_configs import AdxConfigDM
   # …
   ```

Преимущество:
• `entities.py` остается сосредоточенным на основных моделях предметной области.
• Каждая конфигурация индикатора находится в своем собственном файле — ее легче перемещать, тестировать и поддерживать.

<details>
<summary>Original comment in English</summary>

**issue (complexity):** Consider splitting each indicator config dataclass into its own module within a configs subpackage to keep entities.py focused and maintainable.

Here’s one way to thin out the giant `entities.py` while preserving all functionality:

1) Create a new folder, e.g.  
   ```
   domain/
     entities.py
     configs/
       __init__.py
       rsi_configs.py
       adx_configs.py
       …
   ```

2) Move each `@dataclass` into its own module.  
   **domain/configs/adc_configs.py**  
   ```python
   # keep only ADX in here
   from dataclasses import dataclass
   from typing import Any

   @dataclass(slots=True, frozen=True)
   class AdxConfigDM:
       length:      int | None = None
       lensig:      int | None = None
       mamode:      str | None = None
       scalar:      float | int | None = None
       drift:       int | None = None
       offset:      int | None = None
   ```

   **domain/configs/rsi_configs.py**  
   ```python
   from dataclasses import dataclass

   @dataclass(slots=True, frozen=True)
   class RsiCloudsConfigDM:
       rsi_length:  int | None = None
       macd_fast:   int | None = None
       macd_slow:   int | None = None
       macd_signal: int | None = None
       scalar:      float | None = None
       drift:       int | None = None
       offset:      int | None = None
       talib:       bool      = True
   ```

   Repeat for each of the other 10+ configs.

3) In your original `entities.py`, replace all those classes with imports:  
   ```python
   from dataclasses import dataclass
   from typing import Any

   from .configs.rsi_configs import RsiCloudsConfigDM
   from .configs.adx_configs import AdxConfigDM
   # …other imports

   @dataclass(slots=True, frozen=True)
   class CreateTableDM:
       instId:    str
       bar:       str
       partition: str | None = None
   # no more indicator-configs here
   ```

4) (Optional) In `domain/configs/__init__.py` re-export if you prefer:  
   ```python
   from .rsi_configs import RsiCloudsConfigDM
   from .adx_configs import AdxConfigDM
   # …
   ```

Benefit:  
• `entities.py` stays focused on core domain models.  
• Each indicator config lives in its own file—easier to navigate, test, and maintain.

</details>
created_at: 2025-05-11T08:43:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+import pandas_ta as ta
+import pandas as pd
+
+from strategies.src.domain.entities import AdxConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class ADXTrend:
+    """
+    Average Directional Index (ADX) Trend Indicator Implementation.
+    The ADX measures the strength of a trend and is 
+    composed of three components:
+    - **ADX**: Indicates the trend strength 
+      (values above 25 suggest a strong trend).
+    - **DMP (+DI)**: Positive directional movement 
+      (upward trend indicator).
+    - **DMN (-DI)**: Negative directional movement 
+      (downward trend indicator).
+    Formula:
+    1. Compute **True Range (TR)**, which reflects market volatility.
+    2. Calculate the **Directional Movement (+DM / -DM)** based on 
+      high/low price action.
+    3. Apply **Moving Averages** to smooth +DM, -DM values.
+    4. Compute **ADX** as an exponentially smoothed average 
+      of the Directional Index.
+    Usage:
+    - Helps traders determine if a trend is strong 
+      enough to justify entering a trade.
+    - Can be combined with other trend indicators 
+      (e.g., moving averages) for confirmation.
+    """
+
+    def calculate_adx(
+        self, 
+        data: PriceDataFrame, 
+        config: AdxConfigDM
+    ) -> pd.DataFrame:
+        """
+        Computes the ADX, DMP (+DI), and DMN (-DI) trend indicators.
+        Args:
+            data (PriceDataFrame): Price dataset containing 
+              high, low, and close prices.
+            config (AdxConfigDM): Configuration for ADX 
+              (length, smoothing factors, drift, offset).
+        Returns:
+            pd.DataFrame: A DataFrame containing ADX, DMP, and DMN trend values.
+        Raises:
+            ValueError: If the ADX calculation returns missing 
+              values for any required component.
+        """
+        # Compute ADX and its components using pandas-ta
+        adx_ind = ta.adx(
+            high=data.high_prices,
+            low=data.low_prices,
+            close=data.close_prices,
+            length=config.length,
+            # Signal length for ADX smoothing
+            lensig=config.lensig,
+            # Scaling factor for values
+            scalar=config.scalar,  
+            # Moving average mode (e.g., EMA, SMA)
+            mamode=config.mamode,  
+            drift=config.drift,
+            offset=config.offset
+        )
+        #  Validate if all required components exist
+        if missing_indicators := [
+            indicator
+            for indicator in ["ADX", "DMP", "DMN"]
+            if adx_ind.get(f"{indicator}_{config.length}") is None
+        ]:
+            raise ValueError(
+                f"""Error: Missing required ADX components: 
+                {', '.join(missing_indicators)}."""
+            )
+        # Return structured DataFrame
+        return pd.DataFrame(
+            {
+                "adx": adx_ind[f"ADX_{config.lensig}"],
+                "dmp": adx_ind[f"DMP_{config.length}"],
+                "dmn": adx_ind[f"DMN_{config.length}"],
+            },
+            index=data.index
+        )
+
+    def get_signal(self, adx_trigger: int, data: pd.DataFrame) -> bool:
+        """
+        Determines whether the trend strength exceeds a defined threshold.
+        Args:
+            adx_trigger (int): Minimum ADX value required to confirm a trend.
+            data (pd.DataFrame): DataFrame containing ADX values.
+        Returns:
+            bool: True if ADX exceeds the threshold, False otherwise.
+        """
+        return int(data["adx"].iloc[-1]) >= adx_trigger
path: strategies/src/infrastructure/indicators/adx.py
user: login: sourcery-ai[bot]
body: **suggestion:** Возврат int вместо bool может вводить в заблуждение.

Рассмотрите возможность возврата логического значения (True/False) вместо целого числа, чтобы соответствовать строке документации и повысить ясность.

<details>
<summary>Original comment in English</summary>

**suggestion:** Returning int instead of bool may be misleading.

Consider returning a boolean (True/False) instead of an integer to match the docstring and improve clarity.

</details>
created_at: 2025-05-11T08:48:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,201 @@
+import pandas as pd
+import numpy as np
+
+from strategies.src.domain.entities import ScrsiConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class SmoothCicleRsi:
+    """
+    SmoothCicle RSI (SCRSI) Indicator Implementation.
+
+    This indicator calculates an adaptive RSI that includes 
+    smoothing techniques to filter noise.
+    It measures price momentum based on the 
+    difference between closing prices over a 
+    set cycle length.
+
+    Formula:
+    1. Calculate price difference (`diff = close_prices.diff()`).
+    2. Compute the average of positive and negative movements:
+       - up = average gain over `cyclelen` periods.
+       - down = average loss over `cyclelen` periods.
+    3. Standard RSI formula: RSI = 100 - (100 / (1 + up/down)).
+    4. Convert RSI from range [0,100] to [-100,100] to highlight trends.
+    5. Apply cyclic smoothing to refine RSI momentum shifts.
+    6. Define boundaries (`Lower Bound` and `Upper Bound`)
+       based on percentile thresholds.
+
+    Usage:
+    - Provides momentum-based trading signals.
+    - Generates BUY/SELL signals based on 
+      RSI thresholds (crossing level 50, reversals from 0/100).
+    """
+
+    def calculate_scrsi(
+        self, 
+        data: PriceDataFrame, 
+        config: ScrsiConfigDM
+    ) -> pd.DataFrame:
+        """
+        Computes the SmoothCicle RSI (SCRSI) values.
+        Args:
+            data (PriceDataFrame): The price dataset containing close prices.
+            config (ScrsiConfigDM): Indicator configuration (cycle length, 
+            vibration, smoothing).
+        Returns:
+            pd.DataFrame: A DataFrame containing the SCRSI 
+            scaled values and boundary levels.
+        """
+        # Compute cycle length and cyclic memory for smoothing
+        cyclelen = config.domcycle // 2
+        cyclicmemory = config.domcycle * 2
+        # Calculate RSI components: Up (gains) and Down (losses)
+        up = pd.Series(
+            np.maximum(
+                data.close_prices.diff(), 0
+            )
+        ).rolling(window=cyclelen).mean()
+        down = pd.Series(
+            np.minimum(
+                data.close_prices.diff(), 0
+            )
+        ).rolling(window=cyclelen).mean()
+        # Standard RSI calculation with safe division 
+        # (to avoid division by zero issues)
+        rsi = 100 - 100 / (1 + np.divide(
+            up, down, 
+            out=np.zeros_like(up), 
path: strategies/src/infrastructure/indicators/scrsi.py
user: login: sourcery-ai[bot]
body: **issue (bug_risk):** Обработка деления на ноль может маскировать недействительные значения RSI.

Когда down равно нулю, np.divide возвращает 0, делая RSI равным 0, но если up > 0, RSI должно быть 100. Обработайте down==0 явно, чтобы правильно вычислить RSI.

<details>
<summary>Original comment in English</summary>

**issue (bug_risk):** Division by zero handling may mask invalid RSI values.

When down is zero, np.divide returns 0, making RSI 0, but if up > 0, RSI should be 100. Handle down==0 explicitly to compute RSI correctly.

</details>
created_at: 2025-05-11T08:48:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,268 @@
+import pandas as pd
+import numpy as np
+from numba import njit
+from numpy.typing import NDArray
+import pandas_ta as ta
+
+from strategies.src.domain.entities import AvslConfigDM
+from strategies.src.infrastructure._types import PriceDataFrame
+
+
+class AVSL:
+    """
+    Adaptive Volume-Weighted Support Level (
+      AVSL) Indicator Implementation.
+
+    The AVSL indicator calculates 
+      **dynamic support and resistance levels** using 
+      volume-weighted moving averages and 
+      volatility metrics.
+
+    Formula:
+    1. Compute VWMA (**Volume-Weighted Moving Average**) over 
+      fast and slow periods.
+    2. Calculate **Volume Price Confirmation (VPC)** to 
+      measure price deviations.
+    3. Compute **Volume Price Ratio (VPR)** by dividing fast 
+      VWMA by a simple moving average.
+    4. Determine **Volume Momentum (VM)** by comparing fast and 
+      slow volume trends.
+    5. Combine VPC, VPR, and VM into **VPCI** for 
+      trend confirmation.
+    6. Apply a **custom price function** to adjust 
+      support/resistance levels.
+    7. Use a smoothed moving average (**SMA**) to 
+      finalize AVSL levels.
+
+    Usage:
+    - Identifies **strong support zones** based on 
+      price and volume dynamics.
+    - Provides adaptive thresholds for **trend 
+      validation and risk management**.
+    """
+
+    def calculate_avsl(
+        self, 
+        data: PriceDataFrame, 
+        config: AvslConfigDM
+    ) -> pd.DataFrame:
+        """
+        Computes the AVSL (Adaptive Support Level) values.
+        Args:
+            data (PriceDataFrame): Market price dataset containing 
+              high, low, close prices and volume.
+            config (AvslConfigDM): AVSL configuration parameters.
+        Returns:
+            pd.DataFrame: A DataFrame containing AVSL values 
+              indexed by date.
+        """
+        # Compute core components (VWMA, VPC, VPR, VM, VPCI)
+        vw_f, vw_s, vpc, vpr, vm, vpci = self._compute_base_series(data, config)
+        # Compute price adjustment and deviation
+        price_v = self._price_fun(data, vpc, vpr, vpci)
+        deviation = config.stand_div * vpci * vm
+        # Compute AVSL using SMA
+        avsl = ta.sma(
+            close=data.low_prices - price_v + deviation,
+            length=config.length_slow, 
+            talib=True
+        )
+        return pd.DataFrame(
+            {"avsl": avsl}, 
+            index=data.index
+        )
+
+    def _compute_base_series(
+        self, 
+        data: PriceDataFrame, 
+        config: AvslConfigDM
+    ) -> tuple[NDArray]:
+        """
+        Computes base technical indicators required 
+          for AVSL.
+        Args:
+            data (PriceDataFrame): Market price dataset.
+            config (AvslConfigDM): AVSL configuration 
+              parameters.
+        Returns:
+            Tuple[np.ndarray]: VWMA fast, VWMA slow, 
+              VPC, VPR, VM, VPCI.
+        """
+        vw_ma_fast = ta.vwma(
+            close=data.close_prices, 
+            volume=data.volumes, 
+            length=config.length_fast
+        )
+        vw_ma_slow = ta.vwma(
+            close=data.close_prices, 
+            volume=data.volumes, 
+            length=config.length_slow
+        )
+        vpc = vw_ma_slow - ta.sma(
+            close=data.close_prices, 
+            length=config.length_slow, 
+            talib=True
+        )
+        vpr = vw_ma_fast / ta.sma(
+            close=data.close_prices, 
+            length=config.length_fast, 
+            talib=True
+        )
+        vm = ta.sma(
+            close=data.volumes, 
+            length=config.length_fast, 
+            talib=True
+        ) / ta.sma(
+            close=data.volumes, 
+            length=config.length_slow, 
+            talib=True
+        )
+        return (
+            vw_ma_fast, 
+            vw_ma_slow, 
+            vpc, vpr, vm, 
+            vpc * vpr * vm
+        )
+
+    def _price_fun(
+        self, 
+        data: PriceDataFrame, 
+        vpc: pd.Series, 
+        vpr: pd.Series, 
+        vpci: pd.Series
+    ) -> NDArray:
+        """
+        Optimized price adjustment function using vectorized 
+          calculations and a Numba-jitted helper.
+        Args:
+            data (PriceDataFrame): Market price dataset.
+            vpc (pd.Series): Volume Price Confirmation values.
+            vpr (pd.Series): Volume Price Ratio values.
+            vpci (pd.Series): VPCI values.
+        Returns:
+            np.ndarray: Adjusted price levels based on 
+              volume-price interaction.
+        """
+        # Convert pandas Series to NumPy arrays
+        # for faster processing
+        low_np, vpc_np, vpr_np, vpci_np = (
+            s.to_numpy() for s in (data.low_prices, vpc, vpr, vpci)
+        )
+        # Compute `lenV` (Volume Confirmation Length)
+        lenV = self.compute_len_v(vpc_np, vpci_np)
+        # Compute `VPCc` (Adjusted Volume Confirmation Coefficient)
+        VPCc = self.compute_vpcc(vpc_np)
+        # Call the optimized JIT helper function
+        return _compute_price_v(low_np, vpr_np, lenV, VPCc)
+
+    @staticmethod
+    def compute_len_v(
+        vpc: NDArray, 
+        vpci: NDArray
+    ) -> NDArray:
+        """
+        Computes volume confirmation length (`lenV`) 
+          using vectorized calculations.
+        Args:
+            vpc (np.ndarray): Volume Price 
+              Confirmation values.
+            vpci (np.ndarray): VPCI values.
+        Returns:
+            np.ndarray: Computed length values.
+        """
+        return np.where(
+            # Prevent division by zero
+            np.isnan(vpci), 1,  
+            np.where(
+                vpc < 0,
+                np.round(np.abs(vpci - 3)).astype(np.int32),
+                np.round(vpci + 3).astype(np.int32),
+            )
+        )
+
+    @staticmethod
+    def compute_vpcc(vpc: np.ndarray) -> np.ndarray:
+        """
+        Computes adjusted Volume Price 
+          Confirmation Coefficient (`VPCc`).
+        Args:
+            vpc (np.ndarray): Volume Price Confirmation values.
+        Returns:
+            np.ndarray: Adjusted VPC coefficients.
+        """
+        return np.where(
+            (vpc > -1) & (vpc < 0),
+            -1.0,
+            np.where(
+                (
+                    vpc >= 0
+                ) & (
+                    vpc < 1
+                ),
+                1.0, vpc
+            ),
+        )
+
+    def get_last_avsl_signal(
+        self, 
+        data: PriceDataFrame, 
+        config: AvslConfigDM
+    ) -> float | None:
+        """
+        Retrieves the AVSL value on the last bar.
+        Args:
+            data (PriceDataFrame): Market data with price information.
+            config (AvslConfigDM): Configuration settings for AVSL.
+        Returns:
+            float | None: AVSL value of the last bar 
+              if available, otherwise None.
+        """
+        # Compute AVSL values
+        avsl_df = self.calculate_avsl(data, config)
+        # Ensure AVSL is not empty before retrieving the last value
+        if avsl_df.empty or "avsl" not in avsl_df.columns or pd.isna(avsl_df["avsl"].iloc[-1]):
path: strategies/src/infrastructure/indicators/avsl.py
user: login: sourcery-ai[bot]
body: **nitpick:** Проверка существования столбца 'avsl' может быть излишней.

Если calculate_avsl всегда включает столбец 'avsl', вы можете удалить эту проверку; в противном случае выдайте ошибку раньше, чтобы избежать скрытых сбоев.

<details>
<summary>Original comment in English</summary>

**nitpick:** Checking for 'avsl' column existence may be redundant.

If calculate_avsl always includes an 'avsl' column, you can drop that check; otherwise, raise an error earlier to avoid silent failures.

</details>
created_at: 2025-05-11T08:48:39Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-11T08:00:50Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Руководство для проверяющего

Этот pull request представляет несколько новых технических индикаторов (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) путем добавления новых модулей, реструктурирует реализацию RSI Clouds для ясности и расширяемости, обновляет определения типов и конфигурационные dataclass для поддержки новых индикаторов, и соответствующим образом корректирует импорты и зависимости. Изменения также включают переименование и рефакторинг класса PriceDataFrame и связанных свойств типов для обеспечения согласованности и совместимости с новыми индикаторами.

### Изменения на уровне файлов

| Изменение | Детали | Файлы |
| ------ | ------- | ----- |
| Добавление реализаций для новых технических индикаторов: AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX и Order Block Detector. | <ul><li>Представление модуля индикатора AVSL с логикой расчета и сигналов.</li><li>Добавление модуля индикатора SCRSI с расчетом и генерацией сигналов.</li><li>Добавление модуля индикатора Stochastic RSI с расчетом и генерацией сигналов.</li><li>Добавление модуля индикатора Acceleration Bands с расчетом и генерацией сигналов.</li><li>Добавление модуля индикатора тренда ADX с логикой расчета и сигналов.</li><li>Добавление модуля Order Block Detector, использующего пики ZigZag для обнаружения поддержки/сопротивления.</li></ul> | `strategies/src/infrastructure/indicators/avsl.py`<br/>`strategies/src/infrastructure/indicators/scrsi.py`<br/>`strategies/src/infrastructure/indicators/stoch_rsi.py`<br/>`strategies/src/infrastructure/indicators/acceleration_bands.py`<br/>`strategies/src/infrastructure/indicators/adx.py`<br/>`strategies/src/infrastructure/indicators/order_block.py` |
| Реструктуризация и рефакторинг индикатора RSI Clouds для ясности, расширяемости и английской документации. | <ul><li>Переименование класса и методов для ясности и английских наименований.</li><li>Рефакторинг логики для улучшения модульности и документации.</li><li>Обновление методов генерации сигналов и расчета.</li></ul> | `strategies/src/infrastructure/indicators/rsi_clouds.py` |
| Обновление и расширение определений типов и конфигурационных dataclass для поддержки новых индикаторов. | <ul><li>Добавление новых dataclass для конфигурации каждого нового индикатора.</li><li>Рефакторинг класса PriceDataFrame: переименование свойств для согласованности множественного числа, обновление обработки типов и перемещение в _types.py.</li><li>Обновление всех ссылок на новые расположения классов типов и конфигураций.</li></ul> | `strategies/src/domain/entities.py`<br/>`strategies/src/infrastructure/types.py`<br/>`strategies/src/infrastructure/_types.py` |
| Корректировка импортов и ссылок по всему коду для размещения переименования модулей и новых модулей индикаторов. | <ul><li>Обновление импортов в модулях приложения, базы данных и промежуточного слоя для использования новых _types.py и модулей индикаторов.</li></ul> | `main_app/src/infrastructure/middlewares.py`<br/>`main_app/src/infrastructure/models.py`<br/>`strategies/src/application/interfaces.py`<br/>`strategies/src/infrastructure/database.py` |
| Расширение зависимостей проекта для поддержки новых индикаторов и аналитики. | <ul><li>Добавление matplotlib, numba, scipy и yfinance в зависимости в pyproject.toml.</li></ul> | `pyproject.toml` |

---

<details>
<summary>Советы и команды</summary>

#### Взаимодействие с Sourcery

- **Запустить новую проверку:** Напишите комментарий `@sourcery-ai review` в pull request.
- **Продолжить обсуждения:** Отвечайте непосредственно на комментарии Sourcery.
- **Создать issue на GitHub из комментария проверки:** Попросите Sourcery создать
  issue из комментария проверки, ответив на него. Вы также можете ответить на
  комментарий проверки с помощью `@sourcery-ai issue`, чтобы создать issue из него.
- **Сгенерировать заголовок pull request:** Напишите `@sourcery-ai` в любом месте в
  заголовке pull request, чтобы сгенерировать заголовок в любое время. Вы также можете прокомментировать
  `@sourcery-ai title` в pull request, чтобы (повторно) сгенерировать заголовок в любое время.
- **Сгенерировать краткое описание pull request:** Напишите `@sourcery-ai summary` в любом месте
  в теле pull request, чтобы сгенерировать краткое описание PR в любое время именно там, где вы
  хотите. Вы также можете прокомментировать `@sourcery-ai summary` в pull request, чтобы
  (повторно) сгенерировать краткое описание в любое время.
- **Сгенерировать руководство для проверяющего:** Напишите комментарий `@sourcery-ai guide` в pull
  request, чтобы (повторно) сгенерировать руководство для проверяющего в любое время.
- **Разрешить все комментарии Sourcery:** Напишите комментарий `@sourcery-ai resolve` в pull
  request, чтобы разрешить все комментарии Sourcery. Полезно, если вы уже
  рассмотрели все комментарии и больше не хотите их видеть.
- **Отклонить все проверки Sourcery:** Напишите комментарий `@sourcery-ai dismiss` в pull
  request, чтобы отклонить все существующие проверки Sourcery. Особенно полезно, если вы
  хотите начать все заново с новой проверки - не забудьте прокомментировать
  `@sourcery-ai review`, чтобы запустить новую проверку!

#### Настройка вашего опыта

Перейдите на свою [панель управления](https://app.sourcery.ai), чтобы:
- Включить или отключить функции проверки, такие как сгенерированное Sourcery краткое описание pull request,
  руководство для проверяющего и другие.
- Изменить язык проверки.
- Добавить, удалить или изменить пользовательские инструкции по проверке.
- Настроить другие параметры проверки.

#### Получение помощи

- [Свяжитесь с нашей службой поддержки](mailto:support@sourcery.ai) с вопросами или отзывами.
- Посетите нашу [документацию](https://docs.sourcery.ai) для получения подробных руководств и информации.
- Оставайтесь на связи с командой Sourcery, подписавшись на нас в [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) или [GitHub](https://github.com/sourcery-ai).

</details>

<details>
<summary>Original review guide in English</summary>

## Reviewer's Guide

This pull request introduces several new technical indicators (AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, Order Block Detector) by adding new modules, restructures the RSI Clouds implementation for clarity and extensibility, updates type definitions and configuration dataclasses to support the new indicators, and adjusts imports and dependencies accordingly. The changes also include renaming and refactoring of the PriceDataFrame class and related type properties to ensure consistency and compatibility with the new indicators.

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Add implementations for new technical indicators: AVSL, SCRSI, Stochastic RSI, Acceleration Bands, ADX, and Order Block Detector. | <ul><li>Introduce AVSL indicator module with calculation and signal logic.</li><li>Add SCRSI indicator module with calculation and signal generation.</li><li>Add Stochastic RSI indicator module with calculation and signal generation.</li><li>Add Acceleration Bands indicator module with calculation and signal generation.</li><li>Add ADX trend indicator module with calculation and signal logic.</li><li>Add Order Block Detector module using ZigZag peaks for support/resistance detection.</li></ul> | `strategies/src/infrastructure/indicators/avsl.py`<br/>`strategies/src/infrastructure/indicators/scrsi.py`<br/>`strategies/src/infrastructure/indicators/stoch_rsi.py`<br/>`strategies/src/infrastructure/indicators/acceleration_bands.py`<br/>`strategies/src/infrastructure/indicators/adx.py`<br/>`strategies/src/infrastructure/indicators/order_block.py` |
| Restructure and refactor RSI Clouds indicator for clarity, extensibility, and English documentation. | <ul><li>Rename class and methods for clarity and English naming.</li><li>Refactor logic to improve modularity and documentation.</li><li>Update signal generation and calculation methods.</li></ul> | `strategies/src/infrastructure/indicators/rsi_clouds.py` |
| Update and expand type definitions and configuration dataclasses to support new indicators. | <ul><li>Add new dataclasses for each new indicator's configuration.</li><li>Refactor PriceDataFrame class: rename properties for plural consistency, update type handling, and move to _types.py.</li><li>Update all references to new type and config class locations.</li></ul> | `strategies/src/domain/entities.py`<br/>`strategies/src/infrastructure/types.py`<br/>`strategies/src/infrastructure/_types.py` |
| Adjust imports and references throughout the codebase to accommodate module renaming and new indicator modules. | <ul><li>Update imports in application, database, and middleware modules to use new _types.py and indicator modules.</li></ul> | `main_app/src/infrastructure/middlewares.py`<br/>`main_app/src/infrastructure/models.py`<br/>`strategies/src/application/interfaces.py`<br/>`strategies/src/infrastructure/database.py` |
| Expand project dependencies to support new indicators and analytics. | <ul><li>Add matplotlib, numba, scipy, and yfinance to dependencies in pyproject.toml.</li></ul> | `pyproject.toml` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: Pavel14701
created_at: 2025-05-11T08:01:01Z
body: @sourcery-ai title
author_association: OWNER
user: login: Pavel14701
created_at: 2025-05-11T08:01:17Z
body: @sourcery-ai review
author_association: OWNER
user: login: Pavel14701
created_at: 2025-05-11T08:01:35Z
body: @sourcery-ai summary
author_association: OWNER
user: login: Pavel14701
created_at: 2025-05-11T08:01:49Z
body: @sourcery-ai guide
author_association: OWNER
user: login: Pavel14701
created_at: 2025-05-11T08:08:49Z
body: @sourcery-ai review
author_association: OWNER
user: login: Pavel14701
created_at: 2025-05-11T08:35:17Z
body: @sourcery-ai review
author_association: OWNER
user: login: Pavel14701
created_at: 2025-05-11T08:40:07Z
body: @sourcery-ai review
author_association: OWNER