This document outlines the architectural decisions, system design, and engineering tradeoffs made in building the AlphaRatio Scanner. It is intended for software engineers, quantitative developers, and open-source contributors looking to understand the system's inner workings.
The scanner operates as a modular, linear data pipeline executed periodically (e.g., daily via cron). The architecture strictly separates data ingestion, business logic (engines), persistence, and presentation (alerts).
graph TD
%% External Inputs
subgraph External
YF[yfinance API]
TV[TradingView CSVs]
Cron[Cron Scheduler]
end
%% Configuration & State
subgraph Core
Config[config.yaml]
DB[(SQLite DB)]
ConfigLoader[Config Loader]
end
%% Pipeline Stages
subgraph Pipeline [Execution Pipeline - main.py]
Ingest[Data Ingestion Stage]
Resample[Timeframe Resampler]
Engine1[Ratio Engine]
Engine2[Indicator Engine]
Engine3[Levels & Ranking]
Watchlist[Watchlist Engine]
end
%% Presentation
subgraph Alerting
TG[Telegram Bot API]
Snapshots[Snapshot Manager]
end
Cron --> |Triggers| Pipeline
Config --> ConfigLoader
ConfigLoader --> Pipeline
YF --> Ingest
TV --> Ingest
Ingest --> DB
Ingest --> Resample
Resample --> Engine1
Engine1 --> Engine2
Engine2 --> Engine3
Engine3 --> Watchlist
Watchlist --> DB
Watchlist --> TG
Watchlist --> Snapshots
- Configuration Sync: On initialization,
main.pyreadsconfig.yaml, ensuring all tracked symbols and benchmarks exist in the SQLite database. - Ingestion & Validation: The pipeline queries the DB for missing dates and fetches the delta from Yahoo Finance (
yfinance).- Smart History Fetching: To ensure new symbols added to
config.yamlare immediately functional, the system checks the database for existing records. If fewer than 60 records exist, it triggers aperiod="max"fetch; otherwise, it performs an incremental5dupdate. - Smart Invalidation: If a corporate action (like a split) changes historical Adjusted Close prices, the system detects the divergence and automatically triggers a historical backfill to maintain data integrity.
- Manual Ingestion Fallback: For assets restricted by third-party APIs (e.g., secondary indices on Yahoo Finance), the system supports manual OHLCV ingestion via CSV. This workflow integrates with the configuration layer, automatically syncing database records if the symbol is present in
config.yamlbut missing from the schema.
- Smart History Fetching: To ensure new symbols added to
- Resampling: Daily Open-High-Low-Close-Volume (OHLCV) data is resampled into Weekly and Monthly data frames using Pandas.
- Vectorized Computation:
- Ratio Engine: Divides asset OHLC by benchmark OHLC.
- Indicator Engine: Applies RSI, EMA, and Volatility bands to the ratio data (not just absolute price).
- Levels Engine: Calculates Rolling 52-week Highs and All-Time Highs (ATH).
- Ranking: Cross-sectional percentiles are calculated (e.g., Rank 99 means the asset is outperforming 99% of the universe).
- State Evaluation: The Watchlist Engine compares new rankings against historical states to detect transitions (Entry/Exit).
- Dispatch: State changes are dispatched to Telegram.
Decision: We use SQLite via SQLAlchemy rather than PostgreSQL or a specialized time-series database (like InfluxDB or QuestDB). Tradeoff:
- Pros: Zero setup, zero networking overhead, single file portability, and ACID compliance. Ideal for a CLI/Cron-first tool.
- Cons: Poor write concurrency.
- Justification: For EOD (End of Day) data across a few thousand symbols, the entire dataset fits easily into RAM, and writes are batched linearly during the daily run. A time-series DB would be overkill and complicate the developer experience.
Decision: System states, symbols, and parameters are driven by a single config.yaml.
Tradeoff:
- Pros: Highly readable, supports comments, allows traders to manage lists without touching Python code.
- Cons: Lacks strict typing compared to a GUI or JSON Schema.
- Justification: Traders are accustomed to config files. We mitigate YAML's downsides by using Pydantic (planned) or strict dict validation at startup to fail fast on malformed configs.
Decision: The computation pipeline is purely synchronous, while network dispatch (Telegram) is async.
Tradeoff:
- Justification: Pandas operations release the GIL for heavy C-level vectorization, but Pandas itself is not async-friendly. Therefore, multi-threading/async during the CPU-bound calculation phase adds overhead without benefit. We use
asynciostrictly for I/O bound network alerts at the end of the pipeline.
Decision: Indicators and rankings are strictly calculated on Close prices, and alerts are dispatched after market close.
Justification: To maintain quant-grade reliability, the Resampler strictly groups by standard calendar weeks/months, and forward-filling (ffill) is heavily restricted to avoid data leakage from future dates into historical backtests.
graph LR
main --> config_loader
main --> database
main --> seed_database
main --> resampler
main --> ratio_engine
ratio_engine --> models
indicator_engine --> models
levels_engine --> models
ranking_engine --> models
watchlist_engine --> models
watchlist_engine --> alert_engine
alert_engine --> telegram_bot
We adhere to a "Fail Fast, Alert Loudly, Recover Gracefully" philosophy:
- Critical Ingestion Failures: If benchmark data fails to download (e.g., NIFTY50), the entire pipeline halts, and an error is pushed to Telegram. We cannot calculate relative strength without the denominator.
- Partial Ingestion Failures: If an individual asset fails to download, the pipeline logs a warning, skips the asset, and processes the rest of the universe.
- Database Locks: Addressed via batch transactions (
Session.bulk_save_objects) and connection pooling parameters tailored for SQLite (timeout=30).
While currently optimized for a solo developer/trader on a single VPS, the architecture is designed to scale:
- Dashboard / API Layer (Planned):
- Introducing
FastAPIto serve DB states. - Moving from SQLite to
PostgreSQLfor concurrent Read/Write capabilities.
- Introducing
- Message Queues (Planned):
- Migrating from linear batch processing to a worker model using
CeleryorRQ(Redis Queue) to handle Intraday (1m/5m/15m) data fetching and concurrent ratio computations.
- Migrating from linear batch processing to a worker model using
- Plugin Architecture:
- Refactoring the engines into an abstract
BaseScannerinterface, allowing open-source contributors to drop a new.pyfile into aplugins/directory to automatically register new technical indicators.
- Refactoring the engines into an abstract