From df23dd03c3d3c0f6967888baa4de8ae77cf08aca Mon Sep 17 00:00:00 2001 From: Pramit Dutta Date: Mon, 30 Mar 2026 17:48:16 +0530 Subject: [PATCH] Cleanup: remove embedded options project and adopt src layout for FX engine --- .gitignore | 1 - README.md | 22 +- options_quant_engine/.github/workflows/ci.yml | 47 --- options_quant_engine/README.md | 193 ------------ options_quant_engine/config/data_sources.yaml | 50 --- options_quant_engine/config/features.yaml | 28 -- options_quant_engine/config/models.yaml | 22 -- options_quant_engine/config/output.yaml | 5 - options_quant_engine/config/regimes.yaml | 15 - options_quant_engine/config/risk.yaml | 13 - options_quant_engine/config/universe.yaml | 12 - .../examples/sample_signal.json | 28 -- .../options_quant_engine/__init__.py | 5 - .../options_quant_engine/backtest/__init__.py | 3 - .../options_quant_engine/backtest/engine.py | 54 ---- .../options_quant_engine/engine.py | 210 ------------ .../evaluation/__init__.py | 3 - .../options_quant_engine/evaluation/engine.py | 67 ---- .../options_quant_engine/features/__init__.py | 3 - .../options_quant_engine/features/base.py | 27 -- .../options_quant_engine/features/builtins.py | 164 ---------- .../options_quant_engine/features/pipeline.py | 26 -- .../ingestion/__init__.py | 21 -- .../ingestion/adapters.py | 298 ------------------ .../options_quant_engine/ingestion/base.py | 24 -- .../ingestion/credentials.py | 36 --- .../ingestion/http_client.py | 38 --- .../options_quant_engine/ingestion/router.py | 108 ------- .../integration/__init__.py | 3 - .../options_quant_engine/integration/hooks.py | 26 -- .../options_quant_engine/models/__init__.py | 4 - .../options_quant_engine/models/ensemble.py | 44 --- .../options_quant_engine/models/extensions.py | 15 - .../models/relative_value.py | 91 ------ .../options_quant_engine/outputs/__init__.py | 3 - .../options_quant_engine/outputs/formatter.py | 37 --- .../preprocessing/__init__.py | 3 - .../preprocessing/cleaning.py | 12 - .../options_quant_engine/regime/__init__.py | 3 - .../options_quant_engine/regime/engine.py | 87 ----- .../options_quant_engine/risk/__init__.py | 3 - .../options_quant_engine/risk/engine.py | 55 ---- .../options_quant_engine/schemas.py | 78 ----- .../options_quant_engine/signals/__init__.py | 4 - .../signals/confidence.py | 39 --- .../options_quant_engine/signals/engine.py | 74 ----- .../options_quant_engine/utils/__init__.py | 4 - .../options_quant_engine/utils/config.py | 27 -- .../options_quant_engine/utils/logging.py | 14 - .../options_quant_engine/utils/time.py | 16 - options_quant_engine/pyproject.toml | 41 --- options_quant_engine/scripts/run_engine.py | 35 -- .../tests/test_config_loading.py | 11 - .../tests/test_credentials.py | 20 -- .../tests/test_engine_output.py | 46 --- .../tests/test_relative_value_model.py | 23 -- .../tests/test_router_fallback.py | 35 -- pyproject.toml | 2 +- scripts/run_engine.py | 8 +- .../fx_quant_engine}/__init__.py | 0 .../fx_quant_engine}/backtest/__init__.py | 0 .../fx_quant_engine}/backtest/engine.py | 0 .../fx_quant_engine}/engine.py | 0 .../fx_quant_engine}/evaluation/__init__.py | 0 .../fx_quant_engine}/evaluation/engine.py | 0 .../fx_quant_engine}/features/__init__.py | 0 .../fx_quant_engine}/features/base.py | 0 .../fx_quant_engine}/features/builtins.py | 0 .../fx_quant_engine}/features/pipeline.py | 0 .../fx_quant_engine}/ingestion/__init__.py | 0 .../fx_quant_engine}/ingestion/adapters.py | 0 .../fx_quant_engine}/ingestion/base.py | 0 .../fx_quant_engine}/ingestion/credentials.py | 0 .../fx_quant_engine}/ingestion/http_client.py | 0 .../fx_quant_engine}/ingestion/parsers.py | 0 .../fx_quant_engine}/ingestion/router.py | 0 .../fx_quant_engine}/integration/__init__.py | 0 .../fx_quant_engine}/integration/hooks.py | 0 .../fx_quant_engine}/models/__init__.py | 0 .../fx_quant_engine}/models/ensemble.py | 0 .../fx_quant_engine}/models/extensions.py | 0 .../fx_quant_engine}/models/relative_value.py | 0 .../fx_quant_engine}/outputs/__init__.py | 0 .../fx_quant_engine}/outputs/formatter.py | 0 .../preprocessing/__init__.py | 0 .../preprocessing/cleaning.py | 0 .../fx_quant_engine}/regime/__init__.py | 0 .../fx_quant_engine}/regime/engine.py | 0 .../fx_quant_engine}/risk/__init__.py | 0 .../fx_quant_engine}/risk/engine.py | 0 .../fx_quant_engine}/schemas.py | 0 .../fx_quant_engine}/signals/__init__.py | 0 .../fx_quant_engine}/signals/confidence.py | 0 .../fx_quant_engine}/signals/engine.py | 0 .../fx_quant_engine}/utils/__init__.py | 0 .../fx_quant_engine}/utils/config.py | 0 .../fx_quant_engine}/utils/logging.py | 0 .../fx_quant_engine}/utils/time.py | 0 98 files changed, 19 insertions(+), 2367 deletions(-) delete mode 100644 options_quant_engine/.github/workflows/ci.yml delete mode 100644 options_quant_engine/README.md delete mode 100644 options_quant_engine/config/data_sources.yaml delete mode 100644 options_quant_engine/config/features.yaml delete mode 100644 options_quant_engine/config/models.yaml delete mode 100644 options_quant_engine/config/output.yaml delete mode 100644 options_quant_engine/config/regimes.yaml delete mode 100644 options_quant_engine/config/risk.yaml delete mode 100644 options_quant_engine/config/universe.yaml delete mode 100644 options_quant_engine/examples/sample_signal.json delete mode 100644 options_quant_engine/options_quant_engine/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/backtest/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/backtest/engine.py delete mode 100644 options_quant_engine/options_quant_engine/engine.py delete mode 100644 options_quant_engine/options_quant_engine/evaluation/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/evaluation/engine.py delete mode 100644 options_quant_engine/options_quant_engine/features/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/features/base.py delete mode 100644 options_quant_engine/options_quant_engine/features/builtins.py delete mode 100644 options_quant_engine/options_quant_engine/features/pipeline.py delete mode 100644 options_quant_engine/options_quant_engine/ingestion/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/ingestion/adapters.py delete mode 100644 options_quant_engine/options_quant_engine/ingestion/base.py delete mode 100644 options_quant_engine/options_quant_engine/ingestion/credentials.py delete mode 100644 options_quant_engine/options_quant_engine/ingestion/http_client.py delete mode 100644 options_quant_engine/options_quant_engine/ingestion/router.py delete mode 100644 options_quant_engine/options_quant_engine/integration/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/integration/hooks.py delete mode 100644 options_quant_engine/options_quant_engine/models/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/models/ensemble.py delete mode 100644 options_quant_engine/options_quant_engine/models/extensions.py delete mode 100644 options_quant_engine/options_quant_engine/models/relative_value.py delete mode 100644 options_quant_engine/options_quant_engine/outputs/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/outputs/formatter.py delete mode 100644 options_quant_engine/options_quant_engine/preprocessing/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/preprocessing/cleaning.py delete mode 100644 options_quant_engine/options_quant_engine/regime/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/regime/engine.py delete mode 100644 options_quant_engine/options_quant_engine/risk/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/risk/engine.py delete mode 100644 options_quant_engine/options_quant_engine/schemas.py delete mode 100644 options_quant_engine/options_quant_engine/signals/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/signals/confidence.py delete mode 100644 options_quant_engine/options_quant_engine/signals/engine.py delete mode 100644 options_quant_engine/options_quant_engine/utils/__init__.py delete mode 100644 options_quant_engine/options_quant_engine/utils/config.py delete mode 100644 options_quant_engine/options_quant_engine/utils/logging.py delete mode 100644 options_quant_engine/options_quant_engine/utils/time.py delete mode 100644 options_quant_engine/pyproject.toml delete mode 100644 options_quant_engine/scripts/run_engine.py delete mode 100644 options_quant_engine/tests/test_config_loading.py delete mode 100644 options_quant_engine/tests/test_credentials.py delete mode 100644 options_quant_engine/tests/test_engine_output.py delete mode 100644 options_quant_engine/tests/test_relative_value_model.py delete mode 100644 options_quant_engine/tests/test_router_fallback.py rename {fx_quant_engine => src/fx_quant_engine}/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/backtest/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/backtest/engine.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/engine.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/evaluation/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/evaluation/engine.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/features/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/features/base.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/features/builtins.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/features/pipeline.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/ingestion/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/ingestion/adapters.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/ingestion/base.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/ingestion/credentials.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/ingestion/http_client.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/ingestion/parsers.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/ingestion/router.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/integration/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/integration/hooks.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/models/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/models/ensemble.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/models/extensions.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/models/relative_value.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/outputs/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/outputs/formatter.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/preprocessing/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/preprocessing/cleaning.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/regime/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/regime/engine.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/risk/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/risk/engine.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/schemas.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/signals/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/signals/confidence.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/signals/engine.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/utils/__init__.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/utils/config.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/utils/logging.py (100%) rename {fx_quant_engine => src/fx_quant_engine}/utils/time.py (100%) diff --git a/.gitignore b/.gitignore index a00cb7d..59a24c6 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ dist/ # Local run artifacts examples/runs/ -options_quant_engine/examples/runs/ # IDE .vscode/ diff --git a/README.md b/README.md index 83d6643..458d8ec 100644 --- a/README.md +++ b/README.md @@ -34,17 +34,17 @@ Secondary G10 pairs: ## Repository Structure -- fx_quant_engine/ingestion: adapters + fallback router -- fx_quant_engine/preprocessing: data cleaning/alignment -- fx_quant_engine/features: registry + modular feature pipeline -- fx_quant_engine/regime: explainable regime detection -- fx_quant_engine/models: model scoring and extension points -- fx_quant_engine/signals: directional/RV signal + confidence engine -- fx_quant_engine/risk: risk overlays and action mapping -- fx_quant_engine/evaluation: signal quality diagnostics -- fx_quant_engine/backtest: no-lookahead pair/portfolio simulation -- fx_quant_engine/outputs: machine-readable + trader-readable payloads -- fx_quant_engine/integration: cross-engine score exports +- src/fx_quant_engine/ingestion: adapters + fallback router +- src/fx_quant_engine/preprocessing: data cleaning/alignment +- src/fx_quant_engine/features: registry + modular feature pipeline +- src/fx_quant_engine/regime: explainable regime detection +- src/fx_quant_engine/models: model scoring and extension points +- src/fx_quant_engine/signals: directional/RV signal + confidence engine +- src/fx_quant_engine/risk: risk overlays and action mapping +- src/fx_quant_engine/evaluation: signal quality diagnostics +- src/fx_quant_engine/backtest: no-lookahead pair/portfolio simulation +- src/fx_quant_engine/outputs: machine-readable + trader-readable payloads +- src/fx_quant_engine/integration: cross-engine score exports - config: YAML-driven behavior - scripts: runnable examples - tests: unit tests diff --git a/options_quant_engine/.github/workflows/ci.yml b/options_quant_engine/.github/workflows/ci.yml deleted file mode 100644 index f867a40..0000000 --- a/options_quant_engine/.github/workflows/ci.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: options-quant-engine-ci - -on: - push: - paths: - - "options_quant_engine/**" - pull_request: - paths: - - "options_quant_engine/**" - workflow_dispatch: - -jobs: - lint-test-build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: options_quant_engine - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e .[dev] - - - name: Lint - run: ruff check . - - - name: Test - run: pytest - - - name: Generate sample run artifact - run: python scripts/run_engine.py - - - name: Upload run artifacts - uses: actions/upload-artifact@v4 - with: - name: options-engine-runs - path: options_quant_engine/examples/runs/*.json - if-no-files-found: warn diff --git a/options_quant_engine/README.md b/options_quant_engine/README.md deleted file mode 100644 index b646cae..0000000 --- a/options_quant_engine/README.md +++ /dev/null @@ -1,193 +0,0 @@ -# options_quant_engine - -A professional, modular, explainable, and production-ready options quant engine with a clean, extensible architecture for standalone deployment. - -## What This Engine Does - -- Ingests market/macro/rate data from multiple pluggable adapters -- Uses configurable source priority and fallback logic per asset -- Engineers economically meaningful features through a registry-based pipeline -- Detects interpretable market regimes -- Generates directional and relative-value signals -- Separates signal strength from confidence -- Applies risk overlays and outputs action recommendations -- Supports evaluation and realistic backtest scaffolding -- Exports integration hooks for cross-engine multi-asset platforms - -## Underlying Universe - -Primary INR pairs: - -- USDINR -- EURINR -- GBPINR -- JPYINR - -Secondary G10 pairs: - -- EURUSD -- GBPUSD -- USDJPY -- AUDUSD -- USDCAD -- USDCHF - -## Repository Structure - -- options_quant_engine/ingestion: adapters + fallback router -- options_quant_engine/preprocessing: data cleaning/alignment -- options_quant_engine/features: registry + modular feature pipeline -- options_quant_engine/regime: explainable regime detection -- options_quant_engine/models: model scoring and extension points -- options_quant_engine/signals: directional/RV signal + confidence engine -- options_quant_engine/risk: risk overlays and action mapping -- options_quant_engine/evaluation: signal quality diagnostics -- options_quant_engine/backtest: no-lookahead pair/portfolio simulation -- options_quant_engine/outputs: machine-readable + trader-readable payloads -- options_quant_engine/integration: cross-engine score exports -- config: YAML-driven behavior -- scripts: runnable examples -- tests: unit tests -- examples: sample payloads and run artifacts - -## Data Adapter Architecture - -Base interface: - -```python -class BaseDataAdapter: - def fetch_price_data(self, asset, start, end): - pass - - def fetch_macro_data(self, key, start, end): - pass - - def fetch_rate_data(self, asset, start, end): - pass - - def health_check(self): - pass -``` - -Included adapters: - -- BreezeAdapter (ICICI Breeze) -- ZerodhaAdapter (optional via config) -- NSEAdapter -- RBIAdapter -- FreeFXAdapter -- MockAdapter - -Live API stubs are environment-driven and include strict timeout/retry logic: - -- OQE_HTTP_TIMEOUT_SEC -- OQE_HTTP_MAX_ATTEMPTS -- OQE_HTTP_BACKOFF_SEC -- BREEZE_LIVE_ENABLED, BREEZE_BASE_URL, BREEZE_API_KEY, BREEZE_API_SECRET -- ZERODHA_LIVE_ENABLED, ZERODHA_BASE_URL, ZERODHA_API_KEY, ZERODHA_ACCESS_TOKEN -- NSE_LIVE_ENABLED, NSE_BASE_URL -- RBI_LIVE_ENABLED, RBI_BASE_URL - -## Config-Driven Files - -- config/universe.yaml -- config/data_sources.yaml -- config/features.yaml -- config/regimes.yaml -- config/models.yaml -- config/risk.yaml -- config/output.yaml - -## Quickstart - -1. Create and activate a Python 3.10+ environment. -1. Install package and dev dependencies: - -```bash -pip install -e .[dev] -``` - -1. Run tests: - -```bash -pytest -``` - -1. Run engine example: - -```bash -python scripts/run_engine.py -``` - -The script saves artifacts to examples/runs. - -## Signal Output Schema - -Each signal includes: - -- direction -- signal strength -- confidence (separate module) -- regime context -- expected volatility -- key drivers -- risk flags -- recommended action -- position sizing multiplier - -See examples/sample_signal.json. - -## Relative Value Spread Model - -Relative-value signal generation uses a true spread framework: - -- explicit hedge ratio estimation from aligned log prices -- spread z-score signal component -- pair-level momentum and carry differentials -- expected spread volatility and long/short leg assignment - -The output includes `hedge_ratio` and `spread_zscore` in RV payloads. - -## CI - -GitHub Actions workflow: `.github/workflows/ci.yml` - -- ruff lint checks -- pytest execution -- sample run generation -- artifact upload of `examples/runs/*.json` - -## Build Phases Coverage - -Phase 1: - -- repo structure -- config system -- ingestion + preprocessing -- basic features - -Phase 2: - -- regime detection -- signal engine -- confidence engine -- outputs - -Phase 3: - -- risk layer -- evaluation framework -- backtesting scaffolding - -Phase 4 starter: - -- model extension hooks -- integration hooks -- tests -- docs - -## Notes - -- No credentials are hardcoded. -- Live adapter wiring is stubbed with production-style retries/timeouts and env credentials. -- Mock pathways remain default for deterministic testing and offline development. diff --git a/options_quant_engine/config/data_sources.yaml b/options_quant_engine/config/data_sources.yaml deleted file mode 100644 index db59d5f..0000000 --- a/options_quant_engine/config/data_sources.yaml +++ /dev/null @@ -1,50 +0,0 @@ -enabled_sources: - breeze: true - zerodha: false - nse: true - rbi: true - freefx: true - mock: true - -source_priority: - default: [nse, rbi, freefx, mock] - g10: [freefx, mock] - -asset_source_map: - USDINR: [nse, breeze, rbi, mock] - EURINR: [nse, breeze, freefx, mock] - GBPINR: [nse, freefx, mock] - JPYINR: [nse, freefx, mock] - EURUSD: [freefx, mock] - GBPUSD: [freefx, mock] - USDJPY: [freefx, mock] - AUDUSD: [freefx, mock] - USDCAD: [freefx, mock] - USDCHF: [freefx, mock] - -reliability_tags: - breeze: medium - zerodha: medium - nse: high - rbi: high - freefx: medium - mock: low - -latency_tags_ms: - breeze: 350 - zerodha: 250 - nse: 500 - rbi: 800 - freefx: 700 - mock: 1 - -live_api: - breeze_live_enabled: false - zerodha_live_enabled: false - nse_live_enabled: false - rbi_live_enabled: false - -http_controls: - timeout_sec: 5 - max_attempts: 3 - backoff_sec: 0.5 diff --git a/options_quant_engine/config/features.yaml b/options_quant_engine/config/features.yaml deleted file mode 100644 index 1a1abc7..0000000 --- a/options_quant_engine/config/features.yaml +++ /dev/null @@ -1,28 +0,0 @@ -features: - returns: true - moving_averages: true - breakouts: true - trend_strength: true - momentum_multi_horizon: true - momentum_persistence: true - momentum_acceleration: true - mean_reversion_zscore: true - bollinger_position: true - realized_vol: true - atr_proxy: true - vol_regime_proxy: true - carry_proxy: true - usd_strength_score: true - risk_on_off_proxy: true - inr_crude_proxy: true - em_stress_proxy: true - inr_vol_clustering: true - rbi_intervention_proxy: true - cross_asset_bond_proxy: true - cross_asset_commodity_proxy: true - cross_asset_equity_proxy: true - -windows: - short: 5 - medium: 20 - long: 60 diff --git a/options_quant_engine/config/models.yaml b/options_quant_engine/config/models.yaml deleted file mode 100644 index 48273a4..0000000 --- a/options_quant_engine/config/models.yaml +++ /dev/null @@ -1,22 +0,0 @@ -directional: - model_weights: - trend: 0.35 - momentum: 0.25 - mean_reversion: 0.15 - carry: 0.10 - macro: 0.15 - -relative_value: - lookback: 60 - spread_zscore_threshold: 1.0 - model_weights: - spread: 0.5 - momentum_diff: 0.3 - carry_diff: 0.2 - -confidence: - agreement_weight: 0.25 - regime_stability_weight: 0.20 - data_quality_weight: 0.20 - volatility_penalty_weight: 0.20 - event_penalty_weight: 0.15 diff --git a/options_quant_engine/config/output.yaml b/options_quant_engine/config/output.yaml deleted file mode 100644 index a8a9df9..0000000 --- a/options_quant_engine/config/output.yaml +++ /dev/null @@ -1,5 +0,0 @@ -output: - include_dashboard_payload: true - include_trader_summary: true - precision: 4 - engine_name: options_quant_engine diff --git a/options_quant_engine/config/regimes.yaml b/options_quant_engine/config/regimes.yaml deleted file mode 100644 index dcff4d5..0000000 --- a/options_quant_engine/config/regimes.yaml +++ /dev/null @@ -1,15 +0,0 @@ -trend: - slope_threshold: 0.0005 - mr_band: 0.003 - -volatility: - low_quantile: 0.3 - high_quantile: 0.7 - stress_quantile: 0.9 - -dollar: - neutral_band: 0.2 - -risk: - stress_vol_threshold: 0.75 - risk_off_equity_threshold: -0.01 diff --git a/options_quant_engine/config/risk.yaml b/options_quant_engine/config/risk.yaml deleted file mode 100644 index 9876314..0000000 --- a/options_quant_engine/config/risk.yaml +++ /dev/null @@ -1,13 +0,0 @@ -risk: - target_vol: 0.10 - max_gross_exposure: 1.0 - max_single_asset_exposure: 0.25 - stressed_regime_multiplier: 0.5 - illiquid_market_multiplier: 0.6 - data_reliability_penalty: - high: 1.0 - medium: 0.85 - low: 0.6 - market_hours: - inr_derivatives_open_utc: "03:45" - inr_derivatives_close_utc: "10:00" diff --git a/options_quant_engine/config/universe.yaml b/options_quant_engine/config/universe.yaml deleted file mode 100644 index 40830b8..0000000 --- a/options_quant_engine/config/universe.yaml +++ /dev/null @@ -1,12 +0,0 @@ -primary_inr_pairs: - - USDINR - - EURINR - - GBPINR - - JPYINR -secondary_g10_pairs: - - EURUSD - - GBPUSD - - USDJPY - - AUDUSD - - USDCAD - - USDCHF diff --git a/options_quant_engine/examples/sample_signal.json b/options_quant_engine/examples/sample_signal.json deleted file mode 100644 index 72953a8..0000000 --- a/options_quant_engine/examples/sample_signal.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "engine": "options_quant_engine", - "timestamp": "2026-03-30T10:00:00", - "asset": "USDINR", - "signal_type": "directional", - "signal_direction": "long_usd", - "signal_strength": 0.74, - "confidence": 0.68, - "regime": { - "trend": "trend", - "volatility": "normal", - "dollar": "strengthening", - "risk": "risk_off", - "combined": "usd_dominant", - "stability": 0.71 - }, - "expected_volatility": 0.089, - "risk_flags": ["outside_primary_market_hours"], - "drivers": { - "trend": 0.51, - "momentum": 0.36, - "mean_reversion": -0.12, - "carry": 0.09, - "macro": -0.07 - }, - "recommended_action": "enter_small", - "position_sizing_multiplier": 0.65 -} diff --git a/options_quant_engine/options_quant_engine/__init__.py b/options_quant_engine/options_quant_engine/__init__.py deleted file mode 100644 index 7aea006..0000000 --- a/options_quant_engine/options_quant_engine/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Options Quant Engine package.""" - -from options_quant_engine.engine import OptionsQuantEngine - -__all__ = ["OptionsQuantEngine"] diff --git a/options_quant_engine/options_quant_engine/backtest/__init__.py b/options_quant_engine/options_quant_engine/backtest/__init__.py deleted file mode 100644 index 63c6c4a..0000000 --- a/options_quant_engine/options_quant_engine/backtest/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.backtest.engine import BacktestEngine - -__all__ = ["BacktestEngine"] diff --git a/options_quant_engine/options_quant_engine/backtest/engine.py b/options_quant_engine/options_quant_engine/backtest/engine.py deleted file mode 100644 index 7b47bfb..0000000 --- a/options_quant_engine/options_quant_engine/backtest/engine.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pandas as pd - -from options_quant_engine.schemas import BacktestResult - - -class BacktestEngine: - def __init__(self, transaction_cost_bps: float = 1.5, slippage_bps: float = 1.0) -> None: - self.tc = transaction_cost_bps / 10000.0 - self.slippage = slippage_bps / 10000.0 - - def simulate_pair(self, prices: pd.Series, signal_strength: pd.Series) -> pd.Series: - returns = prices.pct_change().fillna(0.0) - position = signal_strength.shift(1).fillna(0.0) # no lookahead - traded = position.diff().abs().fillna(0.0) - net = position * returns - traded * (self.tc + self.slippage) - return net - - def simulate_portfolio(self, pair_returns: dict[str, pd.Series]) -> BacktestResult: - if not pair_returns: - return BacktestResult({}, 0.0, 0.0, 0.0, {}) - - df = pd.DataFrame(pair_returns).fillna(0.0) - port = df.mean(axis=1) - equity = (1.0 + port).cumprod() - dd = equity / equity.cummax() - 1.0 - - pair_level = {k: float(v.mean() * 252.0) for k, v in pair_returns.items()} - metrics = { - "annualized_return": float(port.mean() * 252.0), - "annualized_vol": float(port.std() * np.sqrt(252.0)), - "sharpe": float((port.mean() / (port.std() + 1e-9)) * np.sqrt(252.0)), - } - - return BacktestResult( - pair_level_returns=pair_level, - portfolio_return=float((equity.iloc[-1] - 1.0) if len(equity) else 0.0), - max_drawdown=float(dd.min() if len(dd) else 0.0), - turnover=float(df.diff().abs().sum().sum()), - metrics=metrics, - ) - - def walk_forward(self, prices: pd.Series, signal_strength: pd.Series, train_window: int = 120, test_window: int = 20) -> pd.Series: - out = [] - idx = prices.index - i = train_window - while i < len(idx): - end = min(i + test_window, len(idx)) - segment = self.simulate_pair(prices.iloc[:end], signal_strength.iloc[:end]) - out.append(segment.iloc[i:end]) - i += test_window - return pd.concat(out).sort_index() if out else pd.Series(dtype=float) diff --git a/options_quant_engine/options_quant_engine/engine.py b/options_quant_engine/options_quant_engine/engine.py deleted file mode 100644 index 6cce34e..0000000 --- a/options_quant_engine/options_quant_engine/engine.py +++ /dev/null @@ -1,210 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone -from pathlib import Path -from typing import Any - -import numpy as np -import pandas as pd - -from options_quant_engine.features.pipeline import FeaturePipeline -from options_quant_engine.ingestion.adapters import ( - BreezeAdapter, - FreeFXAdapter, - MockAdapter, - NSEAdapter, - RBIAdapter, - ZerodhaAdapter, -) -from options_quant_engine.ingestion.router import DataSourceRouter -from options_quant_engine.integration.hooks import IntegrationHooks -from options_quant_engine.models.ensemble import EnsembleModel -from options_quant_engine.models.relative_value import RelativeValueModel -from options_quant_engine.outputs.formatter import OutputFormatter -from options_quant_engine.preprocessing.cleaning import preprocess_market_data -from options_quant_engine.regime.engine import RegimeEngine -from options_quant_engine.risk.engine import RiskEngine -from options_quant_engine.schemas import SignalPayload -from options_quant_engine.signals.confidence import ConfidenceEngine -from options_quant_engine.signals.engine import SignalEngine -from options_quant_engine.utils.config import load_all_configs - - -class OptionsQuantEngine: - def __init__(self, config_dir: str | Path = "config") -> None: - self.config = load_all_configs(config_dir) - self.adapters = { - "breeze": BreezeAdapter(), - "zerodha": ZerodhaAdapter(), - "nse": NSEAdapter(), - "rbi": RBIAdapter(), - "freefx": FreeFXAdapter(), - "mock": MockAdapter(), - } - self.router = DataSourceRouter(self.adapters, self.config["data_sources"]) - self.features = FeaturePipeline(self.config["features"]) - self.regimes = RegimeEngine(self.config["regimes"]) - self.models = EnsembleModel(self.config["models"]) - self.rv_model = RelativeValueModel(lookback=int(self.config["models"].get("relative_value", {}).get("lookback", 60))) - self.confidence = ConfidenceEngine(self.config["models"]) - self.signals = SignalEngine(engine_name=self.config["output"]["output"].get("engine_name", "options_quant_engine")) - self.risk = RiskEngine(self.config["risk"]) - self.output = OutputFormatter(self.config["output"]) - self.integration = IntegrationHooks() - - def _asset_context(self, asset: str, start: datetime, end: datetime) -> dict[str, Any]: - px_res = self.router.fetch_price_data(asset, start, end) - macro_res = self.router.fetch_macro_data("macro_proxy", start, end) - rate_res = self.router.fetch_rate_data(asset, start, end) - - if not px_res.success or px_res.data.empty: - raise RuntimeError(f"No price data available for {asset}: {px_res.error}") - - df = px_res.data.join(macro_res.data, how="left").join(rate_res.data, how="left") - if f"{asset}_rate" in df.columns: - df["short_rate"] = df[f"{asset}_rate"] - df["usd_rate"] = 0.045 - df["equity_proxy"] = df["close"].pct_change().rolling(5).mean().fillna(0.0) - df["crude_proxy"] = (1.0 + df["close"].pct_change().fillna(0.0)).cumprod() - df["emfx_proxy"] = df["close"].pct_change().fillna(0.0) - - clean = preprocess_market_data(df) - feat = self.features.transform(clean) - regime = self.regimes.detect(feat) - return { - "price": px_res, - "macro": macro_res, - "rate": rate_res, - "clean": clean, - "features": feat, - "regime": regime, - } - - def run_asset(self, asset: str, start: datetime, end: datetime) -> dict[str, Any]: - ctx = self._asset_context(asset, start, end) - px_res = ctx["price"] - feat = ctx["features"] - regime = ctx["regime"] - row = feat.iloc[-1] - - score, model_drivers, model_agreement = self.models.directional_score(row) - feature_agreement = float((row > 0).mean()) - data_quality = 1.0 if px_res.reliability == "high" else 0.8 if px_res.reliability == "medium" else 0.6 - volatility = float(min(1.0, row.get("realized_vol", 0.0))) - event_risk = float(min(1.0, row.get("rbi_intervention_proxy", 0.0))) - - conf = self.confidence.compute( - feature_agreement=feature_agreement, - model_agreement=model_agreement, - regime_stability=regime.stability, - data_quality=data_quality, - volatility=volatility, - event_risk=event_risk, - ) - - sig: SignalPayload = self.signals.directional_signal( - asset=asset, - feature_row=row, - regime=regime, - score=score, - confidence=conf, - drivers=model_drivers, - expected_volatility=float(row.get("realized_vol", 0.0)), - ) - sig = self.risk.apply( - signal=sig, - regime=regime, - reliability=px_res.reliability, - liquidity_ok=True, - event_risk=event_risk, - ) - - payload = self.output.to_json_payload(sig) - summary = self.output.trader_summary(sig) - dashboard = self.output.dashboard_payload(sig) - hooks = self.integration.export_scores(feat, regime) - - return { - "signal": payload, - "summary": summary, - "dashboard": dashboard, - "integration_hooks": hooks, - "source_usage": [u.__dict__ for u in self.router.usage_log], - } - - def run_relative_value(self, asset_a: str, asset_b: str, start: datetime, end: datetime) -> dict[str, Any]: - ctx_a = self._asset_context(asset_a, start, end) - ctx_b = self._asset_context(asset_b, start, end) - feat_a = ctx_a["features"] - feat_b = ctx_b["features"] - - rv = self.rv_model.generate( - asset_a=asset_a, - asset_b=asset_b, - prices_a=ctx_a["clean"]["close"], - prices_b=ctx_b["clean"]["close"], - features_a=feat_a, - features_b=feat_b, - ) - - regime = ctx_a["regime"] if ctx_a["regime"].stability >= ctx_b["regime"].stability else ctx_b["regime"] - driver_vals = [v for k, v in rv.drivers.items() if k != "hedge_ratio"] - signs = [np.sign(v) for v in driver_vals if abs(v) > 1e-12] - model_agreement = float(abs(sum(signs)) / len(signs)) if signs else 0.0 - feature_agreement = float((feat_a.iloc[-1] - feat_b.iloc[-1] > 0).mean()) - - rel_map = {"high": 1.0, "medium": 0.8, "low": 0.6} - q_a = rel_map.get(ctx_a["price"].reliability, 0.6) - q_b = rel_map.get(ctx_b["price"].reliability, 0.6) - data_quality = float((q_a + q_b) / 2.0) - - volatility = float(min(1.0, rv.expected_volatility)) - event_risk = float( - min( - 1.0, - ( - float(feat_a.iloc[-1].get("rbi_intervention_proxy", 0.0)) - + float(feat_b.iloc[-1].get("rbi_intervention_proxy", 0.0)) - ) - / 2.0, - ) - ) - - conf = self.confidence.compute( - feature_agreement=feature_agreement, - model_agreement=model_agreement, - regime_stability=regime.stability, - data_quality=data_quality, - volatility=volatility, - event_risk=event_risk, - ) - - sig = self.signals.relative_value_signal( - long_asset=rv.long_asset, - short_asset=rv.short_asset, - regime=regime, - score=rv.score, - confidence=conf, - drivers=rv.drivers, - expected_volatility=rv.expected_volatility, - ) - reliability = "high" if min(q_a, q_b) >= 1.0 else "medium" if min(q_a, q_b) >= 0.8 else "low" - sig = self.risk.apply( - signal=sig, - regime=regime, - reliability=reliability, - liquidity_ok=True, - event_risk=event_risk, - ) - - return { - "relative_value_pair": f"{asset_a}:{asset_b}", - "long": rv.long_asset, - "short": rv.short_asset, - "spread_score": rv.score, - "hedge_ratio": rv.hedge_ratio, - "spread_zscore": rv.spread_zscore, - "signal": self.output.to_json_payload(sig), - "summary": self.output.trader_summary(sig), - "dashboard": self.output.dashboard_payload(sig), - } diff --git a/options_quant_engine/options_quant_engine/evaluation/__init__.py b/options_quant_engine/options_quant_engine/evaluation/__init__.py deleted file mode 100644 index 5b4ea85..0000000 --- a/options_quant_engine/options_quant_engine/evaluation/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.evaluation.engine import Evaluator - -__all__ = ["Evaluator"] diff --git a/options_quant_engine/options_quant_engine/evaluation/engine.py b/options_quant_engine/options_quant_engine/evaluation/engine.py deleted file mode 100644 index 207e36a..0000000 --- a/options_quant_engine/options_quant_engine/evaluation/engine.py +++ /dev/null @@ -1,67 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone - -import numpy as np -import pandas as pd - -from options_quant_engine.schemas import EvaluationReport - - -class Evaluator: - def evaluate( - self, - signals: pd.DataFrame, - forward_returns: pd.Series, - regimes: pd.Series, - feature_importance: dict[str, float], - ) -> EvaluationReport: - if signals.empty or forward_returns.empty: - return EvaluationReport( - generated_at=datetime.now(timezone.utc).isoformat(), - hit_rate=0.0, - avg_forward_return=0.0, - regime_performance={}, - signal_decay={}, - feature_importance=feature_importance, - drift_flags=["insufficient_data"], - calibration_score=0.0, - ) - - aligned = signals.join(forward_returns.rename("fwd"), how="inner") - aligned = aligned.join(regimes.rename("regime"), how="left").fillna("unknown") - pred = np.sign(aligned["signal_strength"] * aligned.get("signal_sign", 1.0)) - realized = np.sign(aligned["fwd"]) - hit_rate = float((pred == realized).mean()) - avg_ret = float(aligned["fwd"].mean()) - - regime_perf = { - r: float(v) - for r, v in aligned.groupby("regime")["fwd"].mean().to_dict().items() - } - - signal_decay = { - "1d": float(aligned["fwd"].mean()), - "5d": float(aligned["fwd"].rolling(5).mean().dropna().mean() if len(aligned) >= 5 else 0.0), - "20d": float(aligned["fwd"].rolling(20).mean().dropna().mean() if len(aligned) >= 20 else 0.0), - } - - drift = [] - if abs(avg_ret) < 1e-4: - drift.append("low_signal_edge") - if hit_rate < 0.48: - drift.append("hit_rate_deterioration") - - conf = aligned.get("confidence", pd.Series(0.5, index=aligned.index)) - calibration_score = float(np.clip(1.0 - abs(conf.mean() - hit_rate), 0.0, 1.0)) - - return EvaluationReport( - generated_at=datetime.now(timezone.utc).isoformat(), - hit_rate=hit_rate, - avg_forward_return=avg_ret, - regime_performance=regime_perf, - signal_decay=signal_decay, - feature_importance=feature_importance, - drift_flags=drift, - calibration_score=calibration_score, - ) diff --git a/options_quant_engine/options_quant_engine/features/__init__.py b/options_quant_engine/options_quant_engine/features/__init__.py deleted file mode 100644 index b5b552f..0000000 --- a/options_quant_engine/options_quant_engine/features/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.features.pipeline import FeaturePipeline - -__all__ = ["FeaturePipeline"] diff --git a/options_quant_engine/options_quant_engine/features/base.py b/options_quant_engine/options_quant_engine/features/base.py deleted file mode 100644 index 4f7dfb1..0000000 --- a/options_quant_engine/options_quant_engine/features/base.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - -import pandas as pd - -FeatureFn = Callable[[pd.DataFrame, dict[str, Any]], pd.Series] - - -class FeatureRegistry: - def __init__(self) -> None: - self._features: dict[str, FeatureFn] = {} - - def register(self, name: str, fn: FeatureFn) -> None: - if name in self._features: - raise ValueError(f"Feature already registered: {name}") - self._features[name] = fn - - def get(self, name: str) -> FeatureFn: - return self._features[name] - - def names(self) -> list[str]: - return list(self._features.keys()) - - -registry = FeatureRegistry() diff --git a/options_quant_engine/options_quant_engine/features/builtins.py b/options_quant_engine/options_quant_engine/features/builtins.py deleted file mode 100644 index fc4fd33..0000000 --- a/options_quant_engine/options_quant_engine/features/builtins.py +++ /dev/null @@ -1,164 +0,0 @@ -from __future__ import annotations - -import numpy as np -import pandas as pd - -from options_quant_engine.features.base import registry - - -def _close(df: pd.DataFrame) -> pd.Series: - if "close" in df.columns: - return df["close"] - numeric = df.select_dtypes(include="number") - if numeric.empty: - return pd.Series(0.0, index=df.index) - return numeric.iloc[:, 0] - - -def returns(df: pd.DataFrame, cfg: dict) -> pd.Series: - return _close(df).pct_change().fillna(0.0) - - -def moving_averages(df: pd.DataFrame, cfg: dict) -> pd.Series: - w = cfg["windows"]["medium"] - c = _close(df) - return (c / c.rolling(w).mean() - 1.0).fillna(0.0) - - -def breakouts(df: pd.DataFrame, cfg: dict) -> pd.Series: - w = cfg["windows"]["long"] - c = _close(df) - high = c.rolling(w).max() - low = c.rolling(w).min() - rng = (high - low).replace(0.0, np.nan) - return ((c - low) / rng).fillna(0.5) - - -def trend_strength(df: pd.DataFrame, cfg: dict) -> pd.Series: - w = cfg["windows"]["medium"] - r = returns(df, cfg) - return (r.rolling(w).mean() / (r.rolling(w).std() + 1e-9)).fillna(0.0) - - -def momentum_multi_horizon(df: pd.DataFrame, cfg: dict) -> pd.Series: - c = _close(df) - s = cfg["windows"]["short"] - m = cfg["windows"]["medium"] - l = cfg["windows"]["long"] - return ((c.pct_change(s) + c.pct_change(m) + c.pct_change(l)) / 3.0).fillna(0.0) - - -def momentum_persistence(df: pd.DataFrame, cfg: dict) -> pd.Series: - r = returns(df, cfg) - return r.rolling(cfg["windows"]["short"]).apply(lambda x: float((x > 0).mean()), raw=False).fillna(0.5) - - -def momentum_acceleration(df: pd.DataFrame, cfg: dict) -> pd.Series: - m = momentum_multi_horizon(df, cfg) - return m.diff().fillna(0.0) - - -def mean_reversion_zscore(df: pd.DataFrame, cfg: dict) -> pd.Series: - c = _close(df) - w = cfg["windows"]["medium"] - m = c.rolling(w).mean() - s = c.rolling(w).std() - return ((c - m) / (s + 1e-9)).fillna(0.0) - - -def bollinger_position(df: pd.DataFrame, cfg: dict) -> pd.Series: - z = mean_reversion_zscore(df, cfg) - return (z / 2.0).clip(-1, 1) - - -def realized_vol(df: pd.DataFrame, cfg: dict) -> pd.Series: - r = returns(df, cfg) - return (r.rolling(cfg["windows"]["medium"]).std() * np.sqrt(252)).fillna(0.0) - - -def atr_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - c = _close(df) - return c.diff().abs().rolling(cfg["windows"]["short"]).mean().fillna(0.0) - - -def vol_regime_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - vol = realized_vol(df, cfg) - return (vol / (vol.rolling(cfg["windows"]["long"]).mean() + 1e-9)).fillna(1.0) - - -def carry_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - short_r = df.get("short_rate", pd.Series(0.05, index=df.index)) - usd_r = df.get("usd_rate", pd.Series(0.045, index=df.index)) - return (short_r - usd_r).fillna(0.0) - - -def usd_strength_score(df: pd.DataFrame, cfg: dict) -> pd.Series: - r = returns(df, cfg) - return (-r.rolling(cfg["windows"]["medium"]).mean()).fillna(0.0) - - -def risk_on_off_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - eq = df.get("equity_proxy", returns(df, cfg)).fillna(0.0) - vix = df.get("vix_proxy", realized_vol(df, cfg)).fillna(0.0) - return (eq - vix).fillna(0.0) - - -def inr_crude_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - crude = df.get("crude_proxy", pd.Series(0.0, index=df.index)) - return crude.pct_change().fillna(0.0) - - -def em_stress_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - em = df.get("emfx_proxy", returns(df, cfg)).fillna(0.0) - return (-em.rolling(cfg["windows"]["medium"]).mean()).fillna(0.0) - - -def inr_vol_clustering(df: pd.DataFrame, cfg: dict) -> pd.Series: - r = returns(df, cfg) - return r.abs().rolling(cfg["windows"]["short"]).mean().fillna(0.0) - - -def rbi_intervention_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - z = mean_reversion_zscore(df, cfg) - return (z.abs() > 2.0).astype(float) - - -def cross_asset_bond_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - y = df.get("bond_yield_proxy", pd.Series(0.0, index=df.index)) - return y.diff().fillna(0.0) - - -def cross_asset_commodity_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - g = df.get("gold_proxy", pd.Series(0.0, index=df.index)) - o = df.get("oil_proxy", pd.Series(0.0, index=df.index)) - return (g.pct_change().fillna(0.0) + o.pct_change().fillna(0.0)) / 2.0 - - -def cross_asset_equity_proxy(df: pd.DataFrame, cfg: dict) -> pd.Series: - e = df.get("equity_proxy", pd.Series(0.0, index=df.index)) - return e.pct_change().fillna(0.0) - - -def register_builtin_features() -> None: - registry.register("returns", returns) - registry.register("moving_averages", moving_averages) - registry.register("breakouts", breakouts) - registry.register("trend_strength", trend_strength) - registry.register("momentum_multi_horizon", momentum_multi_horizon) - registry.register("momentum_persistence", momentum_persistence) - registry.register("momentum_acceleration", momentum_acceleration) - registry.register("mean_reversion_zscore", mean_reversion_zscore) - registry.register("bollinger_position", bollinger_position) - registry.register("realized_vol", realized_vol) - registry.register("atr_proxy", atr_proxy) - registry.register("vol_regime_proxy", vol_regime_proxy) - registry.register("carry_proxy", carry_proxy) - registry.register("usd_strength_score", usd_strength_score) - registry.register("risk_on_off_proxy", risk_on_off_proxy) - registry.register("inr_crude_proxy", inr_crude_proxy) - registry.register("em_stress_proxy", em_stress_proxy) - registry.register("inr_vol_clustering", inr_vol_clustering) - registry.register("rbi_intervention_proxy", rbi_intervention_proxy) - registry.register("cross_asset_bond_proxy", cross_asset_bond_proxy) - registry.register("cross_asset_commodity_proxy", cross_asset_commodity_proxy) - registry.register("cross_asset_equity_proxy", cross_asset_equity_proxy) diff --git a/options_quant_engine/options_quant_engine/features/pipeline.py b/options_quant_engine/options_quant_engine/features/pipeline.py deleted file mode 100644 index d26ca16..0000000 --- a/options_quant_engine/options_quant_engine/features/pipeline.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import pandas as pd - -from options_quant_engine.features.base import registry -from options_quant_engine.features.builtins import register_builtin_features - - -class FeaturePipeline: - def __init__(self, config: dict[str, Any]) -> None: - self.config = config - if not registry.names(): - register_builtin_features() - - def transform(self, df: pd.DataFrame) -> pd.DataFrame: - feature_cfg = self.config.get("features", {}) - out = pd.DataFrame(index=df.index) - for feature_name, enabled in feature_cfg.items(): - if not enabled: - continue - if feature_name not in registry.names(): - continue - out[feature_name] = registry.get(feature_name)(df, self.config) - return out.fillna(0.0) diff --git a/options_quant_engine/options_quant_engine/ingestion/__init__.py b/options_quant_engine/options_quant_engine/ingestion/__init__.py deleted file mode 100644 index 9b87927..0000000 --- a/options_quant_engine/options_quant_engine/ingestion/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from options_quant_engine.ingestion.adapters import ( - BreezeAdapter, - FreeFXAdapter, - MockAdapter, - NSEAdapter, - RBIAdapter, - ZerodhaAdapter, -) -from options_quant_engine.ingestion.base import BaseDataAdapter -from options_quant_engine.ingestion.router import DataSourceRouter - -__all__ = [ - "BaseDataAdapter", - "BreezeAdapter", - "ZerodhaAdapter", - "NSEAdapter", - "RBIAdapter", - "FreeFXAdapter", - "MockAdapter", - "DataSourceRouter", -] diff --git a/options_quant_engine/options_quant_engine/ingestion/adapters.py b/options_quant_engine/options_quant_engine/ingestion/adapters.py deleted file mode 100644 index 466d8d2..0000000 --- a/options_quant_engine/options_quant_engine/ingestion/adapters.py +++ /dev/null @@ -1,298 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from datetime import datetime -import os -from typing import Any - -import numpy as np -import pandas as pd - -from options_quant_engine.ingestion.base import BaseDataAdapter -from options_quant_engine.ingestion.credentials import APICredentials, load_credentials -from options_quant_engine.ingestion.http_client import HttpClient, RetryConfig - - -def _mock_price_series(asset: str, start: datetime, end: datetime, seed: int) -> pd.DataFrame: - idx = pd.date_range(start=start, end=end, freq="B") - rng = np.random.default_rng(seed) - steps = rng.normal(loc=0.0, scale=0.002, size=len(idx)) - base = 80.0 if asset.endswith("INR") else 1.1 - px = base * np.exp(np.cumsum(steps)) - return pd.DataFrame({"close": px}, index=idx) - - -def _mock_macro_series(key: str, start: datetime, end: datetime, seed: int) -> pd.DataFrame: - idx = pd.date_range(start=start, end=end, freq="B") - rng = np.random.default_rng(seed) - values = rng.normal(loc=0.0, scale=1.0, size=len(idx)) - return pd.DataFrame({key: values}, index=idx) - - -def _mock_rate_series(asset: str, start: datetime, end: datetime, seed: int) -> pd.DataFrame: - idx = pd.date_range(start=start, end=end, freq="B") - rng = np.random.default_rng(seed) - rate = 0.05 + rng.normal(loc=0.0, scale=0.002, size=len(idx)) - return pd.DataFrame({f"{asset}_rate": rate}, index=idx) - - -def _retry_config_from_env() -> RetryConfig: - return RetryConfig( - timeout_sec=float(os.getenv("OQE_HTTP_TIMEOUT_SEC", "5.0")), - max_attempts=int(os.getenv("OQE_HTTP_MAX_ATTEMPTS", "3")), - backoff_sec=float(os.getenv("OQE_HTTP_BACKOFF_SEC", "0.5")), - ) - - -def _to_price_frame(payload: dict[str, Any], start: datetime, end: datetime) -> pd.DataFrame: - records = payload.get("data", []) - if not isinstance(records, list) or not records: - raise RuntimeError("Price payload missing 'data' list") - df = pd.DataFrame(records) - if "timestamp" in df.columns: - df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, errors="coerce") - df = df.dropna(subset=["timestamp"]).set_index("timestamp") - elif "date" in df.columns: - df["date"] = pd.to_datetime(df["date"], utc=True, errors="coerce") - df = df.dropna(subset=["date"]).set_index("date") - else: - idx = pd.date_range(start=start, end=end, periods=len(df), tz="UTC") - df.index = idx - if "close" not in df.columns: - if "price" in df.columns: - df = df.rename(columns={"price": "close"}) - else: - raise RuntimeError("Price payload missing close/price column") - return df[["close"]].sort_index() - - -def _to_series_frame(payload: dict[str, Any], key: str, start: datetime, end: datetime) -> pd.DataFrame: - records = payload.get("data", []) - if not isinstance(records, list) or not records: - raise RuntimeError("Series payload missing 'data' list") - df = pd.DataFrame(records) - if "timestamp" in df.columns: - df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, errors="coerce") - df = df.dropna(subset=["timestamp"]).set_index("timestamp") - elif "date" in df.columns: - df["date"] = pd.to_datetime(df["date"], utc=True, errors="coerce") - df = df.dropna(subset=["date"]).set_index("date") - else: - idx = pd.date_range(start=start, end=end, periods=len(df), tz="UTC") - df.index = idx - if key not in df.columns: - # Accept first numeric column as a tolerant API stub fallback. - numeric = df.select_dtypes(include="number") - if numeric.empty: - raise RuntimeError(f"Series payload missing '{key}'") - return numeric.iloc[:, :1].rename(columns={numeric.columns[0]: key}) - return df[[key]].sort_index() - - -def _live_enabled(env_key: str) -> bool: - return os.getenv(env_key, "false").lower() in {"1", "true", "yes", "on"} - - -@dataclass -class BreezeAdapter(BaseDataAdapter): - use_live: bool = False - base_url: str | None = None - - def __post_init__(self) -> None: - self.use_live = self.use_live or _live_enabled("BREEZE_LIVE_ENABLED") - self.base_url = self.base_url or os.getenv("BREEZE_BASE_URL") - self.client = HttpClient(_retry_config_from_env()) - - def _creds(self) -> APICredentials: - return load_credentials("BREEZE", required=["API_KEY", "API_SECRET"], optional=["SESSION_TOKEN"]) - - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - if not self.base_url: - raise RuntimeError("Breeze live enabled but BREEZE_BASE_URL not configured") - creds = self._creds() - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/v1/price", - params={"symbol": asset, "start": start.isoformat(), "end": end.isoformat()}, - headers={"X-API-KEY": creds.get("api_key", "")}, - ) - return _to_price_frame(payload, start, end) - return _mock_price_series(asset, start, end, seed=1) - - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - if not self.base_url: - raise RuntimeError("Breeze live enabled but BREEZE_BASE_URL not configured") - creds = self._creds() - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/v1/macro", - params={"key": key, "start": start.isoformat(), "end": end.isoformat()}, - headers={"X-API-KEY": creds.get("api_key", "")}, - ) - return _to_series_frame(payload, key, start, end) - return _mock_macro_series(key, start, end, seed=2) - - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - if not self.base_url: - raise RuntimeError("Breeze live enabled but BREEZE_BASE_URL not configured") - creds = self._creds() - col = f"{asset}_rate" - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/v1/rates", - params={"symbol": asset, "start": start.isoformat(), "end": end.isoformat()}, - headers={"X-API-KEY": creds.get("api_key", "")}, - ) - return _to_series_frame(payload, col, start, end) - return _mock_rate_series(asset, start, end, seed=3) - - def health_check(self) -> bool: - if not self.use_live: - return True - return bool(self.base_url) - - -@dataclass -class ZerodhaAdapter(BaseDataAdapter): - use_live: bool = False - base_url: str | None = None - - def __post_init__(self) -> None: - self.use_live = self.use_live or _live_enabled("ZERODHA_LIVE_ENABLED") - self.base_url = self.base_url or os.getenv("ZERODHA_BASE_URL") - self.client = HttpClient(_retry_config_from_env()) - - def _creds(self) -> APICredentials: - return load_credentials("ZERODHA", required=["API_KEY", "ACCESS_TOKEN"]) - - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - if not self.base_url: - raise RuntimeError("Zerodha live enabled but ZERODHA_BASE_URL not configured") - creds = self._creds() - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/instruments/historical", - params={"symbol": asset, "start": start.isoformat(), "end": end.isoformat()}, - headers={ - "X-Kite-Version": "3", - "Authorization": f"token {creds.get('api_key', '')}:{creds.get('access_token', '')}", - }, - ) - return _to_price_frame(payload, start, end) - return _mock_price_series(asset, start, end, seed=4) - - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> pd.DataFrame: - raise NotImplementedError("Zerodha macro data not supported") - - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - raise NotImplementedError("Zerodha rates not supported") - - def health_check(self) -> bool: - if not self.use_live: - return True - return bool(self.base_url) - - -class NSEAdapter(BaseDataAdapter): - def __init__(self, use_live: bool = False, base_url: str | None = None) -> None: - self.use_live = use_live or _live_enabled("NSE_LIVE_ENABLED") - self.base_url = base_url or os.getenv("NSE_BASE_URL", "https://www.nseindia.com") - self.client = HttpClient(_retry_config_from_env()) - - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/historical/foCPV", - params={"symbol": asset, "from": start.strftime("%d-%m-%Y"), "to": end.strftime("%d-%m-%Y")}, - headers={"User-Agent": "options-quant-engine/0.1"}, - ) - return _to_price_frame(payload, start, end) - return _mock_price_series(asset, start, end, seed=5) - - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/allIndices", - headers={"User-Agent": "options-quant-engine/0.1"}, - ) - return _to_series_frame(payload, key, start, end) - return _mock_macro_series(key, start, end, seed=6) - - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - col = f"{asset}_rate" - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/live-analysis-oi-spurts-underlyings", - headers={"User-Agent": "options-quant-engine/0.1"}, - ) - return _to_series_frame(payload, col, start, end) - return _mock_rate_series(asset, start, end, seed=7) - - def health_check(self) -> bool: - return True - - -class RBIAdapter(BaseDataAdapter): - def __init__(self, use_live: bool = False, base_url: str | None = None) -> None: - self.use_live = use_live or _live_enabled("RBI_LIVE_ENABLED") - self.base_url = base_url or os.getenv("RBI_BASE_URL", "https://data.rbi.org.in") - self.client = HttpClient(_retry_config_from_env()) - - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/exchange-rate", - params={"pair": asset, "from": start.isoformat(), "to": end.isoformat()}, - ) - return _to_price_frame(payload, start, end) - return _mock_price_series(asset, start, end, seed=8) - - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/macro", - params={"series": key, "from": start.isoformat(), "to": end.isoformat()}, - ) - return _to_series_frame(payload, key, start, end) - return _mock_macro_series(key, start, end, seed=9) - - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - if self.use_live: - col = f"{asset}_rate" - payload = self.client.get_json( - f"{self.base_url.rstrip('/')}/api/policy-rates", - params={"asset": asset, "from": start.isoformat(), "to": end.isoformat()}, - ) - return _to_series_frame(payload, col, start, end) - return _mock_rate_series(asset, start, end, seed=10) - - def health_check(self) -> bool: - return True - - -class FreeFXAdapter(BaseDataAdapter): - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - return _mock_price_series(asset, start, end, seed=11) - - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> pd.DataFrame: - return _mock_macro_series(key, start, end, seed=12) - - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - return _mock_rate_series(asset, start, end, seed=13) - - def health_check(self) -> bool: - return True - - -class MockAdapter(BaseDataAdapter): - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - return _mock_price_series(asset, start, end, seed=42) - - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> pd.DataFrame: - return _mock_macro_series(key, start, end, seed=43) - - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - return _mock_rate_series(asset, start, end, seed=44) - - def health_check(self) -> bool: - return True diff --git a/options_quant_engine/options_quant_engine/ingestion/base.py b/options_quant_engine/options_quant_engine/ingestion/base.py deleted file mode 100644 index ec654bc..0000000 --- a/options_quant_engine/options_quant_engine/ingestion/base.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from datetime import datetime - -import pandas as pd - - -class BaseDataAdapter(ABC): - @abstractmethod - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - pass - - @abstractmethod - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> pd.DataFrame: - pass - - @abstractmethod - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> pd.DataFrame: - pass - - @abstractmethod - def health_check(self) -> bool: - pass diff --git a/options_quant_engine/options_quant_engine/ingestion/credentials.py b/options_quant_engine/options_quant_engine/ingestion/credentials.py deleted file mode 100644 index 50913bc..0000000 --- a/options_quant_engine/options_quant_engine/ingestion/credentials.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -import os -from dataclasses import dataclass - - -@dataclass(frozen=True) -class APICredentials: - values: dict[str, str] - - def get(self, key: str, default: str | None = None) -> str | None: - return self.values.get(key, default) - - -def load_credentials(prefix: str, required: list[str], optional: list[str] | None = None) -> APICredentials: - optional = optional or [] - payload: dict[str, str] = {} - - missing: list[str] = [] - for key in required: - env_key = f"{prefix}_{key}" - value = os.getenv(env_key) - if not value: - missing.append(env_key) - continue - payload[key.lower()] = value - - for key in optional: - env_key = f"{prefix}_{key}" - value = os.getenv(env_key) - if value: - payload[key.lower()] = value - - if missing: - raise RuntimeError(f"Missing required credentials: {', '.join(missing)}") - return APICredentials(values=payload) diff --git a/options_quant_engine/options_quant_engine/ingestion/http_client.py b/options_quant_engine/options_quant_engine/ingestion/http_client.py deleted file mode 100644 index 164e414..0000000 --- a/options_quant_engine/options_quant_engine/ingestion/http_client.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -import time -from dataclasses import dataclass -from typing import Any - -import requests - - -@dataclass -class RetryConfig: - timeout_sec: float = 5.0 - max_attempts: int = 3 - backoff_sec: float = 0.5 - - -class HttpClient: - def __init__(self, config: RetryConfig) -> None: - self.config = config - - def get_json(self, url: str, params: dict[str, Any] | None = None, headers: dict[str, str] | None = None) -> dict[str, Any]: - last_err = "unknown" - for attempt in range(1, self.config.max_attempts + 1): - try: - response = requests.get(url, params=params, headers=headers, timeout=self.config.timeout_sec) - response.raise_for_status() - data = response.json() - if not isinstance(data, dict): - raise RuntimeError("Expected JSON object response") - return data - except Exception as exc: # noqa: BLE001 - last_err = str(exc) - if attempt < self.config.max_attempts: - time.sleep(self.config.backoff_sec * attempt) - - raise RuntimeError( - f"HTTP GET failed after {self.config.max_attempts} attempts for {url}: {last_err}" - ) diff --git a/options_quant_engine/options_quant_engine/ingestion/router.py b/options_quant_engine/options_quant_engine/ingestion/router.py deleted file mode 100644 index 57926f0..0000000 --- a/options_quant_engine/options_quant_engine/ingestion/router.py +++ /dev/null @@ -1,108 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from typing import Any - -import pandas as pd - -from options_quant_engine.ingestion.base import BaseDataAdapter -from options_quant_engine.schemas import DataFetchResult, SourceUsageRecord -from options_quant_engine.utils.logging import get_logger - - -class DataSourceRouter: - def __init__( - self, - adapters: dict[str, BaseDataAdapter], - config: dict[str, Any], - ) -> None: - self.adapters = adapters - self.config = config - self.logger = get_logger(self.__class__.__name__) - self.usage_log: list[SourceUsageRecord] = [] - - def _get_candidate_sources(self, asset: str) -> list[str]: - source_cfg = self.config - mapped = source_cfg.get("asset_source_map", {}).get(asset) - if mapped: - return mapped - g10 = asset in {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD", "USDCHF"} - key = "g10" if g10 else "default" - return source_cfg.get("source_priority", {}).get(key, ["mock"]) - - def _is_enabled(self, source: str) -> bool: - return bool(self.config.get("enabled_sources", {}).get(source, False)) - - def _tag(self, source: str) -> tuple[str, int]: - reliability = self.config.get("reliability_tags", {}).get(source, "low") - latency_ms = int(self.config.get("latency_tags_ms", {}).get(source, 1000)) - return reliability, latency_ms - - def fetch_price_data(self, asset: str, start: datetime, end: datetime) -> DataFetchResult: - return self._fetch_with_fallback("price", asset, start, end) - - def fetch_macro_data(self, key: str, start: datetime, end: datetime) -> DataFetchResult: - return self._fetch_with_fallback("macro", key, start, end) - - def fetch_rate_data(self, asset: str, start: datetime, end: datetime) -> DataFetchResult: - return self._fetch_with_fallback("rate", asset, start, end) - - def _fetch_with_fallback( - self, - usage_type: str, - key: str, - start: datetime, - end: datetime, - ) -> DataFetchResult: - candidates = self._get_candidate_sources(key) - last_err = "No source attempted" - - for source in candidates: - if not self._is_enabled(source): - continue - adapter = self.adapters.get(source) - if adapter is None: - continue - try: - if usage_type == "price": - data = adapter.fetch_price_data(key, start, end) - elif usage_type == "macro": - data = adapter.fetch_macro_data(key, start, end) - else: - data = adapter.fetch_rate_data(key, start, end) - reliability, latency_ms = self._tag(source) - self.usage_log.append(SourceUsageRecord(asset=key, source=source, usage_type=usage_type)) - self.logger.info("Fetched %s data for %s via %s", usage_type, key, source) - return DataFetchResult( - data=data, - source=source, - reliability=reliability, - latency_ms=latency_ms, - ) - except Exception as exc: # noqa: BLE001 - last_err = str(exc) - self.usage_log.append( - SourceUsageRecord( - asset=key, - source=source, - usage_type=usage_type, - success=False, - error=last_err, - ) - ) - self.logger.warning( - "Failed %s data fetch for %s via %s: %s", - usage_type, - key, - source, - last_err, - ) - - return DataFetchResult( - data=pd.DataFrame(), - source="none", - reliability="low", - latency_ms=9999, - success=False, - error=last_err, - ) diff --git a/options_quant_engine/options_quant_engine/integration/__init__.py b/options_quant_engine/options_quant_engine/integration/__init__.py deleted file mode 100644 index 201d5ac..0000000 --- a/options_quant_engine/options_quant_engine/integration/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.integration.hooks import IntegrationHooks - -__all__ = ["IntegrationHooks"] diff --git a/options_quant_engine/options_quant_engine/integration/hooks.py b/options_quant_engine/options_quant_engine/integration/hooks.py deleted file mode 100644 index 6c297b1..0000000 --- a/options_quant_engine/options_quant_engine/integration/hooks.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import pandas as pd - -from options_quant_engine.schemas import RegimeState - - -class IntegrationHooks: - def export_scores(self, features: pd.DataFrame, regime: RegimeState) -> dict[str, float]: - if features.empty: - return { - "usd_strength_score": 0.0, - "inr_stress_score": 0.0, - "carry_attractiveness": 0.0, - "fx_volatility_stress": 0.0, - "risk_on_off_score": 0.0, - } - row = features.iloc[-1] - return { - "usd_strength_score": float(row.get("usd_strength_score", 0.0)), - "inr_stress_score": float(row.get("em_stress_proxy", 0.0) + row.get("inr_vol_clustering", 0.0)), - "carry_attractiveness": float(row.get("carry_proxy", 0.0)), - "fx_volatility_stress": float(row.get("realized_vol", 0.0)), - "risk_on_off_score": float(row.get("risk_on_off_proxy", 0.0)), - "regime_stability": float(regime.stability), - } diff --git a/options_quant_engine/options_quant_engine/models/__init__.py b/options_quant_engine/options_quant_engine/models/__init__.py deleted file mode 100644 index 7099b0e..0000000 --- a/options_quant_engine/options_quant_engine/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from options_quant_engine.models.ensemble import EnsembleModel -from options_quant_engine.models.relative_value import RelativeValueModel - -__all__ = ["EnsembleModel", "RelativeValueModel"] diff --git a/options_quant_engine/options_quant_engine/models/ensemble.py b/options_quant_engine/options_quant_engine/models/ensemble.py deleted file mode 100644 index 46e8760..0000000 --- a/options_quant_engine/options_quant_engine/models/ensemble.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import numpy as np -import pandas as pd - - -class EnsembleModel: - def __init__(self, config: dict[str, Any]) -> None: - self.config = config - - def directional_score(self, row: pd.Series) -> tuple[float, dict[str, float], float]: - weights = self.config.get("directional", {}).get("model_weights", {}) - component = { - "trend": float(row.get("trend_strength", 0.0)), - "momentum": float(row.get("momentum_multi_horizon", 0.0)), - "mean_reversion": -float(row.get("mean_reversion_zscore", 0.0)), - "carry": float(row.get("carry_proxy", 0.0)), - "macro": float(row.get("risk_on_off_proxy", 0.0)), - } - wsum = sum(float(weights.get(k, 0.0)) for k in component) - score = 0.0 - if wsum > 0: - score = sum(component[k] * float(weights.get(k, 0.0)) for k in component) / wsum - normalized = float(np.tanh(score)) - signs = [np.sign(v) for v in component.values() if abs(v) > 1e-12] - agreement = float((abs(sum(signs)) / len(signs))) if signs else 0.0 - return normalized, component, agreement - - def relative_value_score(self, row_a: pd.Series, row_b: pd.Series) -> tuple[float, dict[str, float], float]: - weights = self.config.get("relative_value", {}).get("model_weights", {}) - spread = float(row_a.get("mean_reversion_zscore", 0.0) - row_b.get("mean_reversion_zscore", 0.0)) - mom_diff = float(row_a.get("momentum_multi_horizon", 0.0) - row_b.get("momentum_multi_horizon", 0.0)) - carry_diff = float(row_a.get("carry_proxy", 0.0) - row_b.get("carry_proxy", 0.0)) - component = {"spread": spread, "momentum_diff": mom_diff, "carry_diff": carry_diff} - wsum = sum(float(weights.get(k, 0.0)) for k in component) - score = 0.0 - if wsum > 0: - score = sum(component[k] * float(weights.get(k, 0.0)) for k in component) / wsum - normalized = float(np.tanh(score)) - signs = [np.sign(v) for v in component.values() if abs(v) > 1e-12] - agreement = float((abs(sum(signs)) / len(signs))) if signs else 0.0 - return normalized, component, agreement diff --git a/options_quant_engine/options_quant_engine/models/extensions.py b/options_quant_engine/options_quant_engine/models/extensions.py deleted file mode 100644 index e1ad3b9..0000000 --- a/options_quant_engine/options_quant_engine/models/extensions.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Placeholder for future model extensions. - -This module is intentionally lightweight in the starter phase. Future additions can -include gradient boosting, probabilistic forecasts, and online learning modules. -""" - -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass -class ModelExtensionConfig: - enabled: bool = False - name: str = "baseline_extension" diff --git a/options_quant_engine/options_quant_engine/models/relative_value.py b/options_quant_engine/options_quant_engine/models/relative_value.py deleted file mode 100644 index 7159a5d..0000000 --- a/options_quant_engine/options_quant_engine/models/relative_value.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass - -import numpy as np -import pandas as pd - - -@dataclass -class RelativeValueResult: - score: float - hedge_ratio: float - spread_zscore: float - expected_volatility: float - drivers: dict[str, float] - long_asset: str - short_asset: str - - -class RelativeValueModel: - def __init__(self, lookback: int = 60) -> None: - self.lookback = lookback - - def generate( - self, - asset_a: str, - asset_b: str, - prices_a: pd.Series, - prices_b: pd.Series, - features_a: pd.DataFrame, - features_b: pd.DataFrame, - ) -> RelativeValueResult: - pa, pb = self._align(prices_a, prices_b) - la = np.log(pa) - lb = np.log(pb) - - hedge_ratio = self._hedge_ratio(la.tail(self.lookback), lb.tail(self.lookback)) - spread = la - hedge_ratio * lb - - window = max(20, self.lookback // 2) - zscore = self._zscore(spread, window=window) - spread_z = float(zscore.iloc[-1]) if len(zscore) else 0.0 - - row_a = features_a.iloc[-1] - row_b = features_b.iloc[-1] - momentum_diff = float(row_a.get("momentum_multi_horizon", 0.0) - row_b.get("momentum_multi_horizon", 0.0)) - carry_diff = float(row_a.get("carry_proxy", 0.0) - row_b.get("carry_proxy", 0.0)) - - # Mean-reversion spread score with feature differentials. - raw = -0.60 * spread_z + 0.25 * momentum_diff + 0.15 * carry_diff - score = float(np.tanh(raw)) - - if score >= 0: - long_asset, short_asset = asset_a, asset_b - else: - long_asset, short_asset = asset_b, asset_a - - expected_vol = float(spread.diff().tail(window).std() * np.sqrt(252.0)) if len(spread) > 2 else 0.0 - drivers = { - "spread_zscore": spread_z, - "momentum_diff": momentum_diff, - "carry_diff": carry_diff, - "hedge_ratio": float(hedge_ratio), - } - return RelativeValueResult( - score=abs(score), - hedge_ratio=float(hedge_ratio), - spread_zscore=spread_z, - expected_volatility=max(expected_vol, 0.0), - drivers=drivers, - long_asset=long_asset, - short_asset=short_asset, - ) - - @staticmethod - def _align(a: pd.Series, b: pd.Series) -> tuple[pd.Series, pd.Series]: - joined = pd.concat([a.rename("a"), b.rename("b")], axis=1).dropna() - return joined["a"], joined["b"] - - @staticmethod - def _hedge_ratio(a: pd.Series, b: pd.Series) -> float: - denom = float(np.var(b)) - if abs(denom) < 1e-12: - return 1.0 - return float(np.cov(a, b)[0, 1] / denom) - - @staticmethod - def _zscore(series: pd.Series, window: int) -> pd.Series: - mu = series.rolling(window).mean() - sigma = series.rolling(window).std() - return ((series - mu) / (sigma + 1e-9)).fillna(0.0) diff --git a/options_quant_engine/options_quant_engine/outputs/__init__.py b/options_quant_engine/options_quant_engine/outputs/__init__.py deleted file mode 100644 index 1af431c..0000000 --- a/options_quant_engine/options_quant_engine/outputs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.outputs.formatter import OutputFormatter - -__all__ = ["OutputFormatter"] diff --git a/options_quant_engine/options_quant_engine/outputs/formatter.py b/options_quant_engine/options_quant_engine/outputs/formatter.py deleted file mode 100644 index 3135625..0000000 --- a/options_quant_engine/options_quant_engine/outputs/formatter.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -from typing import Any - -from options_quant_engine.schemas import SignalPayload - - -class OutputFormatter: - def __init__(self, config: dict[str, Any]) -> None: - self.config = config.get("output", {}) - - def to_json_payload(self, signal: SignalPayload) -> dict[str, Any]: - p = self.config.get("precision", 4) - data = signal.to_dict() - data["signal_strength"] = round(float(data["signal_strength"]), p) - data["confidence"] = round(float(data["confidence"]), p) - data["expected_volatility"] = round(float(data["expected_volatility"]), p) - data["position_sizing_multiplier"] = round(float(data["position_sizing_multiplier"]), p) - return data - - def trader_summary(self, signal: SignalPayload) -> str: - return ( - f"{signal.asset} | {signal.signal_type} | {signal.signal_direction} | " - f"strength={signal.signal_strength:.2f} confidence={signal.confidence:.2f} " - f"action={signal.recommended_action} size={signal.position_sizing_multiplier:.2f} " - f"regime={signal.regime.get('combined', 'neutral')}" - ) - - def dashboard_payload(self, signal: SignalPayload) -> dict[str, Any]: - return { - "asset": signal.asset, - "score": signal.signal_strength, - "confidence": signal.confidence, - "action": signal.recommended_action, - "risk_flags": signal.risk_flags, - "regime": signal.regime, - } diff --git a/options_quant_engine/options_quant_engine/preprocessing/__init__.py b/options_quant_engine/options_quant_engine/preprocessing/__init__.py deleted file mode 100644 index 22a06bb..0000000 --- a/options_quant_engine/options_quant_engine/preprocessing/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.preprocessing.cleaning import preprocess_market_data - -__all__ = ["preprocess_market_data"] diff --git a/options_quant_engine/options_quant_engine/preprocessing/cleaning.py b/options_quant_engine/options_quant_engine/preprocessing/cleaning.py deleted file mode 100644 index 9868a3b..0000000 --- a/options_quant_engine/options_quant_engine/preprocessing/cleaning.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import annotations - -import pandas as pd - - -def preprocess_market_data(df: pd.DataFrame) -> pd.DataFrame: - if df.empty: - return df - out = df.copy().sort_index() - out = out[~out.index.duplicated(keep="last")] - out = out.ffill().dropna(how="all") - return out diff --git a/options_quant_engine/options_quant_engine/regime/__init__.py b/options_quant_engine/options_quant_engine/regime/__init__.py deleted file mode 100644 index 10454f2..0000000 --- a/options_quant_engine/options_quant_engine/regime/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.regime.engine import RegimeEngine - -__all__ = ["RegimeEngine"] diff --git a/options_quant_engine/options_quant_engine/regime/engine.py b/options_quant_engine/options_quant_engine/regime/engine.py deleted file mode 100644 index 10ba54f..0000000 --- a/options_quant_engine/options_quant_engine/regime/engine.py +++ /dev/null @@ -1,87 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import numpy as np -import pandas as pd - -from options_quant_engine.schemas import RegimeState - - -class RegimeEngine: - def __init__(self, config: dict[str, Any]) -> None: - self.config = config - - def detect(self, features: pd.DataFrame) -> RegimeState: - if features.empty: - return RegimeState("neutral", "normal", "neutral", "risk_on", "neutral", 0.0) - - row = features.iloc[-1] - trend = self._trend_regime(row) - vol = self._vol_regime(features) - dollar = self._dollar_regime(row) - risk = self._risk_regime(row, vol) - combined = self._combine(trend, vol, dollar, risk) - stability = self._stability(features) - return RegimeState(trend, vol, dollar, risk, combined, stability) - - def _trend_regime(self, row: pd.Series) -> str: - score = float(row.get("trend_strength", 0.0)) - th = float(self.config.get("trend", {}).get("slope_threshold", 0.0005)) - band = float(self.config.get("trend", {}).get("mr_band", 0.003)) - if score > th: - return "trend" - if score < -band: - return "mean_reversion" - return "neutral" - - def _vol_regime(self, features: pd.DataFrame) -> str: - vol = features.get("realized_vol", pd.Series(0.0, index=features.index)).fillna(0.0) - cur = float(vol.iloc[-1]) - ql = float(vol.quantile(self.config.get("volatility", {}).get("low_quantile", 0.3))) - qh = float(vol.quantile(self.config.get("volatility", {}).get("high_quantile", 0.7))) - qs = float(vol.quantile(self.config.get("volatility", {}).get("stress_quantile", 0.9))) - if cur >= qs: - return "stress" - if cur >= qh: - return "high" - if cur <= ql: - return "low" - return "normal" - - def _dollar_regime(self, row: pd.Series) -> str: - usd = float(row.get("usd_strength_score", 0.0)) - band = float(self.config.get("dollar", {}).get("neutral_band", 0.2)) - if usd > band: - return "strengthening" - if usd < -band: - return "weakening" - return "neutral" - - def _risk_regime(self, row: pd.Series, vol_regime: str) -> str: - risk_score = float(row.get("risk_on_off_proxy", 0.0)) - threshold = float(self.config.get("risk", {}).get("risk_off_equity_threshold", -0.01)) - if vol_regime == "stress": - return "stressed" - if risk_score < threshold: - return "risk_off" - return "risk_on" - - def _combine(self, trend: str, vol: str, dollar: str, risk: str) -> str: - if vol == "stress" or risk == "stressed": - return "defensive" - if trend == "trend" and risk == "risk_on": - return "pro_trend" - if trend == "mean_reversion" and risk != "stressed": - return "mean_revert" - if dollar == "strengthening": - return "usd_dominant" - return "neutral" - - def _stability(self, features: pd.DataFrame) -> float: - cols = [c for c in ["trend_strength", "realized_vol", "usd_strength_score", "risk_on_off_proxy"] if c in features] - if not cols: - return 0.0 - tail = features[cols].tail(10) - vol = float(tail.std().mean()) - return float(np.clip(1.0 - vol, 0.0, 1.0)) diff --git a/options_quant_engine/options_quant_engine/risk/__init__.py b/options_quant_engine/options_quant_engine/risk/__init__.py deleted file mode 100644 index bcf4c4a..0000000 --- a/options_quant_engine/options_quant_engine/risk/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from options_quant_engine.risk.engine import RiskEngine - -__all__ = ["RiskEngine"] diff --git a/options_quant_engine/options_quant_engine/risk/engine.py b/options_quant_engine/options_quant_engine/risk/engine.py deleted file mode 100644 index 24f00dd..0000000 --- a/options_quant_engine/options_quant_engine/risk/engine.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone -from typing import Any - -from options_quant_engine.schemas import RegimeState, SignalPayload -from options_quant_engine.utils.time import is_within_market_hours - - -class RiskEngine: - def __init__(self, config: dict[str, Any]) -> None: - self.config = config.get("risk", {}) - - def apply(self, signal: SignalPayload, regime: RegimeState, reliability: str, liquidity_ok: bool, event_risk: float) -> SignalPayload: - penalties = self.config.get("data_reliability_penalty", {}) - rel_mult = float(penalties.get(reliability, 0.6)) - size = signal.signal_strength * signal.confidence * rel_mult - - if regime.volatility in {"high", "stress"}: - size *= float(self.config.get("stressed_regime_multiplier", 0.5)) - signal.risk_flags.append("high_volatility_regime") - - if not liquidity_ok: - size *= float(self.config.get("illiquid_market_multiplier", 0.6)) - signal.risk_flags.append("liquidity_constraint") - - if event_risk > 0.6: - size *= 0.5 - signal.risk_flags.append("event_risk_high") - - if not self._market_open_now(): - signal.risk_flags.append("outside_primary_market_hours") - size *= 0.8 - - signal.position_sizing_multiplier = max(0.0, min(size, 1.0)) - signal.recommended_action = self._action(signal.position_sizing_multiplier) - return signal - - def _market_open_now(self) -> bool: - now = datetime.now(timezone.utc) - open_utc = self.config.get("market_hours", {}).get("inr_derivatives_open_utc", "03:45") - close_utc = self.config.get("market_hours", {}).get("inr_derivatives_close_utc", "10:00") - return is_within_market_hours(now, open_utc, close_utc) - - @staticmethod - def _action(size: float) -> str: - if size >= 0.75: - return "enter" - if size >= 0.45: - return "enter_small" - if size >= 0.25: - return "hold" - if size > 0.0: - return "reduce" - return "no_trade" diff --git a/options_quant_engine/options_quant_engine/schemas.py b/options_quant_engine/options_quant_engine/schemas.py deleted file mode 100644 index b17067b..0000000 --- a/options_quant_engine/options_quant_engine/schemas.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -from dataclasses import asdict, dataclass, field -from datetime import datetime, timezone -from typing import Any - -import pandas as pd - - -@dataclass -class DataFetchResult: - data: pd.DataFrame - source: str - reliability: str - latency_ms: int - success: bool = True - error: str | None = None - - -@dataclass -class RegimeState: - trend: str - volatility: str - dollar: str - risk: str - combined: str - stability: float - - -@dataclass -class SignalPayload: - engine: str - timestamp: str - asset: str - signal_type: str - signal_direction: str - signal_strength: float - confidence: float - regime: dict[str, Any] - expected_volatility: float - risk_flags: list[str] - drivers: dict[str, Any] - recommended_action: str - position_sizing_multiplier: float - - def to_dict(self) -> dict[str, Any]: - return asdict(self) - - -@dataclass -class EvaluationReport: - generated_at: str - hit_rate: float - avg_forward_return: float - regime_performance: dict[str, float] - signal_decay: dict[str, float] - feature_importance: dict[str, float] - drift_flags: list[str] - calibration_score: float - - -@dataclass -class BacktestResult: - pair_level_returns: dict[str, float] - portfolio_return: float - max_drawdown: float - turnover: float - metrics: dict[str, float] - - -@dataclass -class SourceUsageRecord: - asset: str - source: str - usage_type: str - timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat()) - success: bool = True - error: str | None = None diff --git a/options_quant_engine/options_quant_engine/signals/__init__.py b/options_quant_engine/options_quant_engine/signals/__init__.py deleted file mode 100644 index 81c3792..0000000 --- a/options_quant_engine/options_quant_engine/signals/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from options_quant_engine.signals.confidence import ConfidenceEngine -from options_quant_engine.signals.engine import SignalEngine - -__all__ = ["SignalEngine", "ConfidenceEngine"] diff --git a/options_quant_engine/options_quant_engine/signals/confidence.py b/options_quant_engine/options_quant_engine/signals/confidence.py deleted file mode 100644 index b1efdd1..0000000 --- a/options_quant_engine/options_quant_engine/signals/confidence.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import numpy as np - - -class ConfidenceEngine: - def __init__(self, config: dict[str, Any]) -> None: - self.config = config - - def compute( - self, - feature_agreement: float, - model_agreement: float, - regime_stability: float, - data_quality: float, - volatility: float, - event_risk: float, - ) -> float: - c = self.config.get("confidence", {}) - agreement_w = float(c.get("agreement_weight", 0.25)) - regime_w = float(c.get("regime_stability_weight", 0.20)) - data_w = float(c.get("data_quality_weight", 0.20)) - vol_w = float(c.get("volatility_penalty_weight", 0.20)) - event_w = float(c.get("event_penalty_weight", 0.15)) - - agreement = 0.5 * (feature_agreement + model_agreement) - vol_penalty = np.clip(volatility, 0.0, 1.0) - event_penalty = np.clip(event_risk, 0.0, 1.0) - - raw = ( - agreement_w * agreement - + regime_w * np.clip(regime_stability, 0.0, 1.0) - + data_w * np.clip(data_quality, 0.0, 1.0) - - vol_w * vol_penalty - - event_w * event_penalty - ) - return float(np.clip((raw + 1.0) / 2.0, 0.0, 1.0)) diff --git a/options_quant_engine/options_quant_engine/signals/engine.py b/options_quant_engine/options_quant_engine/signals/engine.py deleted file mode 100644 index cc47e02..0000000 --- a/options_quant_engine/options_quant_engine/signals/engine.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone -from typing import Any - -import numpy as np -import pandas as pd - -from options_quant_engine.schemas import RegimeState, SignalPayload - - -class SignalEngine: - def __init__(self, engine_name: str = "options_quant_engine") -> None: - self.engine_name = engine_name - - def directional_signal( - self, - asset: str, - feature_row: pd.Series, - regime: RegimeState, - score: float, - confidence: float, - drivers: dict[str, float], - expected_volatility: float, - ) -> SignalPayload: - direction = self._direction(asset, score) - return SignalPayload( - engine=self.engine_name, - timestamp=datetime.now(timezone.utc).isoformat(), - asset=asset, - signal_type="directional", - signal_direction=direction, - signal_strength=float(np.clip(abs(score), 0.0, 1.0)), - confidence=float(np.clip(confidence, 0.0, 1.0)), - regime=regime.__dict__, - expected_volatility=float(max(0.0, expected_volatility)), - risk_flags=[], - drivers={k: float(v) for k, v in drivers.items()}, - recommended_action="hold", - position_sizing_multiplier=0.0, - ) - - def relative_value_signal( - self, - long_asset: str, - short_asset: str, - regime: RegimeState, - score: float, - confidence: float, - drivers: dict[str, float], - expected_volatility: float, - ) -> SignalPayload: - direction = f"long_{long_asset}_vs_{short_asset}" - asset = f"{long_asset}:{short_asset}" - return SignalPayload( - engine=self.engine_name, - timestamp=datetime.now(timezone.utc).isoformat(), - asset=asset, - signal_type="relative_value", - signal_direction=direction, - signal_strength=float(np.clip(abs(score), 0.0, 1.0)), - confidence=float(np.clip(confidence, 0.0, 1.0)), - regime=regime.__dict__, - expected_volatility=float(max(0.0, expected_volatility)), - risk_flags=[], - drivers={k: float(v) for k, v in drivers.items()}, - recommended_action="hold", - position_sizing_multiplier=0.0, - ) - - def _direction(self, asset: str, score: float) -> str: - if asset.startswith("USD"): - return "long_usd" if score >= 0 else "short_usd" - return "long_base" if score >= 0 else "short_base" diff --git a/options_quant_engine/options_quant_engine/utils/__init__.py b/options_quant_engine/options_quant_engine/utils/__init__.py deleted file mode 100644 index 5f3dc38..0000000 --- a/options_quant_engine/options_quant_engine/utils/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from options_quant_engine.utils.config import load_all_configs, load_yaml -from options_quant_engine.utils.logging import get_logger - -__all__ = ["load_yaml", "load_all_configs", "get_logger"] diff --git a/options_quant_engine/options_quant_engine/utils/config.py b/options_quant_engine/options_quant_engine/utils/config.py deleted file mode 100644 index d7485d1..0000000 --- a/options_quant_engine/options_quant_engine/utils/config.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import annotations - -from pathlib import Path -from typing import Any - -import yaml - - -def load_yaml(path: str | Path) -> dict[str, Any]: - with Path(path).open("r", encoding="utf-8") as f: - data = yaml.safe_load(f) or {} - if not isinstance(data, dict): - raise ValueError(f"Config file must contain a mapping: {path}") - return data - - -def load_all_configs(config_dir: str | Path) -> dict[str, dict[str, Any]]: - cfg_dir = Path(config_dir) - return { - "universe": load_yaml(cfg_dir / "universe.yaml"), - "data_sources": load_yaml(cfg_dir / "data_sources.yaml"), - "features": load_yaml(cfg_dir / "features.yaml"), - "regimes": load_yaml(cfg_dir / "regimes.yaml"), - "models": load_yaml(cfg_dir / "models.yaml"), - "risk": load_yaml(cfg_dir / "risk.yaml"), - "output": load_yaml(cfg_dir / "output.yaml"), - } diff --git a/options_quant_engine/options_quant_engine/utils/logging.py b/options_quant_engine/options_quant_engine/utils/logging.py deleted file mode 100644 index 92f48c7..0000000 --- a/options_quant_engine/options_quant_engine/utils/logging.py +++ /dev/null @@ -1,14 +0,0 @@ -from __future__ import annotations - -import logging - - -def get_logger(name: str) -> logging.Logger: - logger = logging.getLogger(name) - if not logger.handlers: - handler = logging.StreamHandler() - fmt = "%(asctime)s | %(name)s | %(levelname)s | %(message)s" - handler.setFormatter(logging.Formatter(fmt)) - logger.addHandler(handler) - logger.setLevel(logging.INFO) - return logger diff --git a/options_quant_engine/options_quant_engine/utils/time.py b/options_quant_engine/options_quant_engine/utils/time.py deleted file mode 100644 index d344f11..0000000 --- a/options_quant_engine/options_quant_engine/utils/time.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, time, timezone - - -def is_within_market_hours( - now_utc: datetime, - open_hhmm: str, - close_hhmm: str, -) -> bool: - open_h, open_m = [int(x) for x in open_hhmm.split(":")] - close_h, close_m = [int(x) for x in close_hhmm.split(":")] - open_t = time(hour=open_h, minute=open_m) - close_t = time(hour=close_h, minute=close_m) - t = now_utc.astimezone(timezone.utc).time() - return open_t <= t <= close_t diff --git a/options_quant_engine/pyproject.toml b/options_quant_engine/pyproject.toml deleted file mode 100644 index f5c2392..0000000 --- a/options_quant_engine/pyproject.toml +++ /dev/null @@ -1,41 +0,0 @@ -[build-system] -requires = ["setuptools>=68", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "options-quant-engine" -version = "0.1.0" -description = "Modular, explainable, production-grade options quant engine" -readme = "README.md" -requires-python = ">=3.10" -authors = [{ name = "Options Quant Team" }] -dependencies = [ - "numpy>=1.26", - "pandas>=2.2", - "PyYAML>=6.0", - "scipy>=1.12", - "requests>=2.32" -] - -[project.optional-dependencies] -dev = [ - "pytest>=8.0", - "pytest-cov>=5.0", - "ruff>=0.5" -] - -[tool.setuptools.packages.find] -where = ["."] -include = ["options_quant_engine*"] - -[tool.pytest.ini_options] -testpaths = ["tests"] -addopts = "-q" - -[tool.ruff] -line-length = 100 -target-version = "py310" - -[tool.ruff.lint] -select = ["E", "F", "I", "B", "UP"] -ignore = ["E501"] diff --git a/options_quant_engine/scripts/run_engine.py b/options_quant_engine/scripts/run_engine.py deleted file mode 100644 index 35fcf8b..0000000 --- a/options_quant_engine/scripts/run_engine.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -import json -from datetime import datetime, timedelta, timezone -from pathlib import Path - -from options_quant_engine import OptionsQuantEngine - - -def main() -> None: - root = Path(__file__).resolve().parents[1] - engine = OptionsQuantEngine(config_dir=root / "config") - - end = datetime.now(timezone.utc) - start = end - timedelta(days=260) - - assets = ["USDINR", "EURINR", "GBPINR", "JPYINR", "EURUSD", "USDJPY"] - out_dir = root / "examples" / "runs" - out_dir.mkdir(parents=True, exist_ok=True) - - outputs = {} - for asset in assets: - outputs[asset] = engine.run_asset(asset, start, end) - - rv = engine.run_relative_value("USDJPY", "EURUSD", start, end) - outputs["relative_value"] = rv - - ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") - out_file = out_dir / f"engine_run_{ts}.json" - out_file.write_text(json.dumps(outputs, indent=2), encoding="utf-8") - print(f"Saved engine output to {out_file}") - - -if __name__ == "__main__": - main() diff --git a/options_quant_engine/tests/test_config_loading.py b/options_quant_engine/tests/test_config_loading.py deleted file mode 100644 index 1fddb01..0000000 --- a/options_quant_engine/tests/test_config_loading.py +++ /dev/null @@ -1,11 +0,0 @@ -from pathlib import Path - -from options_quant_engine.utils.config import load_all_configs - - -def test_load_all_configs() -> None: - root = Path(__file__).resolve().parents[1] - cfg = load_all_configs(root / "config") - assert "universe" in cfg - assert "data_sources" in cfg - assert cfg["data_sources"]["enabled_sources"]["nse"] is True diff --git a/options_quant_engine/tests/test_credentials.py b/options_quant_engine/tests/test_credentials.py deleted file mode 100644 index 09d7f55..0000000 --- a/options_quant_engine/tests/test_credentials.py +++ /dev/null @@ -1,20 +0,0 @@ -import os - -import pytest - -from options_quant_engine.ingestion.credentials import load_credentials - - -def test_load_credentials_success(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("BREEZE_API_KEY", "k") - monkeypatch.setenv("BREEZE_API_SECRET", "s") - creds = load_credentials("BREEZE", required=["API_KEY", "API_SECRET"]) - assert creds.get("api_key") == "k" - assert creds.get("api_secret") == "s" - - -def test_load_credentials_missing_required(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("ZERODHA_API_KEY", raising=False) - monkeypatch.delenv("ZERODHA_ACCESS_TOKEN", raising=False) - with pytest.raises(RuntimeError): - load_credentials("ZERODHA", required=["API_KEY", "ACCESS_TOKEN"]) diff --git a/options_quant_engine/tests/test_engine_output.py b/options_quant_engine/tests/test_engine_output.py deleted file mode 100644 index b4844ef..0000000 --- a/options_quant_engine/tests/test_engine_output.py +++ /dev/null @@ -1,46 +0,0 @@ -from datetime import datetime, timedelta, timezone -from pathlib import Path - -from options_quant_engine import OptionsQuantEngine - - -def test_engine_signal_schema() -> None: - root = Path(__file__).resolve().parents[1] - engine = OptionsQuantEngine(config_dir=root / "config") - end = datetime.now(timezone.utc) - start = end - timedelta(days=200) - - out = engine.run_asset("USDINR", start, end) - signal = out["signal"] - - required = { - "engine", - "timestamp", - "asset", - "signal_type", - "signal_direction", - "signal_strength", - "confidence", - "regime", - "expected_volatility", - "risk_flags", - "drivers", - "recommended_action", - "position_sizing_multiplier", - } - assert required.issubset(signal.keys()) - assert 0.0 <= signal["signal_strength"] <= 1.0 - assert 0.0 <= signal["confidence"] <= 1.0 - - -def test_engine_relative_value_schema() -> None: - root = Path(__file__).resolve().parents[1] - engine = OptionsQuantEngine(config_dir=root / "config") - end = datetime.now(timezone.utc) - start = end - timedelta(days=200) - - out = engine.run_relative_value("USDINR", "EURINR", start, end) - assert "hedge_ratio" in out - assert "spread_zscore" in out - assert "signal" in out - assert 0.0 <= out["signal"]["signal_strength"] <= 1.0 diff --git a/options_quant_engine/tests/test_relative_value_model.py b/options_quant_engine/tests/test_relative_value_model.py deleted file mode 100644 index 096959b..0000000 --- a/options_quant_engine/tests/test_relative_value_model.py +++ /dev/null @@ -1,23 +0,0 @@ -import numpy as np -import pandas as pd - -from options_quant_engine.models.relative_value import RelativeValueModel - - -def test_relative_value_model_outputs_hedge_ratio_and_score() -> None: - idx = pd.date_range("2025-01-01", periods=120, freq="B") - base = np.linspace(100, 110, len(idx)) - a = pd.Series(base + np.sin(np.arange(len(idx)) * 0.2), index=idx) - b = pd.Series(base * 0.8 + np.cos(np.arange(len(idx)) * 0.2), index=idx) - - fa = pd.DataFrame({"momentum_multi_horizon": 0.2, "carry_proxy": 0.01}, index=idx) - fb = pd.DataFrame({"momentum_multi_horizon": -0.1, "carry_proxy": -0.02}, index=idx) - - model = RelativeValueModel(lookback=60) - rv = model.generate("USDINR", "EURINR", a, b, fa, fb) - - assert 0.0 <= rv.score <= 1.0 - assert abs(rv.hedge_ratio) > 0.0 - assert rv.long_asset in {"USDINR", "EURINR"} - assert rv.short_asset in {"USDINR", "EURINR"} - assert "hedge_ratio" in rv.drivers diff --git a/options_quant_engine/tests/test_router_fallback.py b/options_quant_engine/tests/test_router_fallback.py deleted file mode 100644 index fb7ba95..0000000 --- a/options_quant_engine/tests/test_router_fallback.py +++ /dev/null @@ -1,35 +0,0 @@ -from datetime import datetime, timedelta, timezone - -from options_quant_engine.ingestion.adapters import MockAdapter -from options_quant_engine.ingestion.base import BaseDataAdapter -from options_quant_engine.ingestion.router import DataSourceRouter - - -class FailingAdapter(BaseDataAdapter): - def fetch_price_data(self, asset, start, end): - raise RuntimeError("fail") - - def fetch_macro_data(self, key, start, end): - raise RuntimeError("fail") - - def fetch_rate_data(self, asset, start, end): - raise RuntimeError("fail") - - def health_check(self): - return False - - -def test_fallback_to_mock_when_primary_fails() -> None: - cfg = { - "enabled_sources": {"nse": True, "mock": True}, - "source_priority": {"default": ["nse", "mock"]}, - "asset_source_map": {"USDINR": ["nse", "mock"]}, - "reliability_tags": {"nse": "high", "mock": "low"}, - "latency_tags_ms": {"nse": 500, "mock": 1}, - } - router = DataSourceRouter({"nse": FailingAdapter(), "mock": MockAdapter()}, cfg) - end = datetime.now(timezone.utc) - start = end - timedelta(days=30) - out = router.fetch_price_data("USDINR", start, end) - assert out.success is True - assert out.source == "mock" diff --git a/pyproject.toml b/pyproject.toml index ceb3bdf..29c7590 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dev = [ ] [tool.setuptools.packages.find] -where = ["."] +where = ["src"] include = ["fx_quant_engine*"] [tool.pytest.ini_options] diff --git a/scripts/run_engine.py b/scripts/run_engine.py index 711bc25..6f90a0a 100644 --- a/scripts/run_engine.py +++ b/scripts/run_engine.py @@ -3,12 +3,18 @@ from __future__ import annotations import json from datetime import datetime, timedelta, timezone from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" +if str(SRC) not in sys.path: + sys.path.insert(0, str(SRC)) from fx_quant_engine import FXQuantEngine def main() -> None: - root = Path(__file__).resolve().parents[1] + root = ROOT engine = FXQuantEngine(config_dir=root / "config") end = datetime.now(timezone.utc) diff --git a/fx_quant_engine/__init__.py b/src/fx_quant_engine/__init__.py similarity index 100% rename from fx_quant_engine/__init__.py rename to src/fx_quant_engine/__init__.py diff --git a/fx_quant_engine/backtest/__init__.py b/src/fx_quant_engine/backtest/__init__.py similarity index 100% rename from fx_quant_engine/backtest/__init__.py rename to src/fx_quant_engine/backtest/__init__.py diff --git a/fx_quant_engine/backtest/engine.py b/src/fx_quant_engine/backtest/engine.py similarity index 100% rename from fx_quant_engine/backtest/engine.py rename to src/fx_quant_engine/backtest/engine.py diff --git a/fx_quant_engine/engine.py b/src/fx_quant_engine/engine.py similarity index 100% rename from fx_quant_engine/engine.py rename to src/fx_quant_engine/engine.py diff --git a/fx_quant_engine/evaluation/__init__.py b/src/fx_quant_engine/evaluation/__init__.py similarity index 100% rename from fx_quant_engine/evaluation/__init__.py rename to src/fx_quant_engine/evaluation/__init__.py diff --git a/fx_quant_engine/evaluation/engine.py b/src/fx_quant_engine/evaluation/engine.py similarity index 100% rename from fx_quant_engine/evaluation/engine.py rename to src/fx_quant_engine/evaluation/engine.py diff --git a/fx_quant_engine/features/__init__.py b/src/fx_quant_engine/features/__init__.py similarity index 100% rename from fx_quant_engine/features/__init__.py rename to src/fx_quant_engine/features/__init__.py diff --git a/fx_quant_engine/features/base.py b/src/fx_quant_engine/features/base.py similarity index 100% rename from fx_quant_engine/features/base.py rename to src/fx_quant_engine/features/base.py diff --git a/fx_quant_engine/features/builtins.py b/src/fx_quant_engine/features/builtins.py similarity index 100% rename from fx_quant_engine/features/builtins.py rename to src/fx_quant_engine/features/builtins.py diff --git a/fx_quant_engine/features/pipeline.py b/src/fx_quant_engine/features/pipeline.py similarity index 100% rename from fx_quant_engine/features/pipeline.py rename to src/fx_quant_engine/features/pipeline.py diff --git a/fx_quant_engine/ingestion/__init__.py b/src/fx_quant_engine/ingestion/__init__.py similarity index 100% rename from fx_quant_engine/ingestion/__init__.py rename to src/fx_quant_engine/ingestion/__init__.py diff --git a/fx_quant_engine/ingestion/adapters.py b/src/fx_quant_engine/ingestion/adapters.py similarity index 100% rename from fx_quant_engine/ingestion/adapters.py rename to src/fx_quant_engine/ingestion/adapters.py diff --git a/fx_quant_engine/ingestion/base.py b/src/fx_quant_engine/ingestion/base.py similarity index 100% rename from fx_quant_engine/ingestion/base.py rename to src/fx_quant_engine/ingestion/base.py diff --git a/fx_quant_engine/ingestion/credentials.py b/src/fx_quant_engine/ingestion/credentials.py similarity index 100% rename from fx_quant_engine/ingestion/credentials.py rename to src/fx_quant_engine/ingestion/credentials.py diff --git a/fx_quant_engine/ingestion/http_client.py b/src/fx_quant_engine/ingestion/http_client.py similarity index 100% rename from fx_quant_engine/ingestion/http_client.py rename to src/fx_quant_engine/ingestion/http_client.py diff --git a/fx_quant_engine/ingestion/parsers.py b/src/fx_quant_engine/ingestion/parsers.py similarity index 100% rename from fx_quant_engine/ingestion/parsers.py rename to src/fx_quant_engine/ingestion/parsers.py diff --git a/fx_quant_engine/ingestion/router.py b/src/fx_quant_engine/ingestion/router.py similarity index 100% rename from fx_quant_engine/ingestion/router.py rename to src/fx_quant_engine/ingestion/router.py diff --git a/fx_quant_engine/integration/__init__.py b/src/fx_quant_engine/integration/__init__.py similarity index 100% rename from fx_quant_engine/integration/__init__.py rename to src/fx_quant_engine/integration/__init__.py diff --git a/fx_quant_engine/integration/hooks.py b/src/fx_quant_engine/integration/hooks.py similarity index 100% rename from fx_quant_engine/integration/hooks.py rename to src/fx_quant_engine/integration/hooks.py diff --git a/fx_quant_engine/models/__init__.py b/src/fx_quant_engine/models/__init__.py similarity index 100% rename from fx_quant_engine/models/__init__.py rename to src/fx_quant_engine/models/__init__.py diff --git a/fx_quant_engine/models/ensemble.py b/src/fx_quant_engine/models/ensemble.py similarity index 100% rename from fx_quant_engine/models/ensemble.py rename to src/fx_quant_engine/models/ensemble.py diff --git a/fx_quant_engine/models/extensions.py b/src/fx_quant_engine/models/extensions.py similarity index 100% rename from fx_quant_engine/models/extensions.py rename to src/fx_quant_engine/models/extensions.py diff --git a/fx_quant_engine/models/relative_value.py b/src/fx_quant_engine/models/relative_value.py similarity index 100% rename from fx_quant_engine/models/relative_value.py rename to src/fx_quant_engine/models/relative_value.py diff --git a/fx_quant_engine/outputs/__init__.py b/src/fx_quant_engine/outputs/__init__.py similarity index 100% rename from fx_quant_engine/outputs/__init__.py rename to src/fx_quant_engine/outputs/__init__.py diff --git a/fx_quant_engine/outputs/formatter.py b/src/fx_quant_engine/outputs/formatter.py similarity index 100% rename from fx_quant_engine/outputs/formatter.py rename to src/fx_quant_engine/outputs/formatter.py diff --git a/fx_quant_engine/preprocessing/__init__.py b/src/fx_quant_engine/preprocessing/__init__.py similarity index 100% rename from fx_quant_engine/preprocessing/__init__.py rename to src/fx_quant_engine/preprocessing/__init__.py diff --git a/fx_quant_engine/preprocessing/cleaning.py b/src/fx_quant_engine/preprocessing/cleaning.py similarity index 100% rename from fx_quant_engine/preprocessing/cleaning.py rename to src/fx_quant_engine/preprocessing/cleaning.py diff --git a/fx_quant_engine/regime/__init__.py b/src/fx_quant_engine/regime/__init__.py similarity index 100% rename from fx_quant_engine/regime/__init__.py rename to src/fx_quant_engine/regime/__init__.py diff --git a/fx_quant_engine/regime/engine.py b/src/fx_quant_engine/regime/engine.py similarity index 100% rename from fx_quant_engine/regime/engine.py rename to src/fx_quant_engine/regime/engine.py diff --git a/fx_quant_engine/risk/__init__.py b/src/fx_quant_engine/risk/__init__.py similarity index 100% rename from fx_quant_engine/risk/__init__.py rename to src/fx_quant_engine/risk/__init__.py diff --git a/fx_quant_engine/risk/engine.py b/src/fx_quant_engine/risk/engine.py similarity index 100% rename from fx_quant_engine/risk/engine.py rename to src/fx_quant_engine/risk/engine.py diff --git a/fx_quant_engine/schemas.py b/src/fx_quant_engine/schemas.py similarity index 100% rename from fx_quant_engine/schemas.py rename to src/fx_quant_engine/schemas.py diff --git a/fx_quant_engine/signals/__init__.py b/src/fx_quant_engine/signals/__init__.py similarity index 100% rename from fx_quant_engine/signals/__init__.py rename to src/fx_quant_engine/signals/__init__.py diff --git a/fx_quant_engine/signals/confidence.py b/src/fx_quant_engine/signals/confidence.py similarity index 100% rename from fx_quant_engine/signals/confidence.py rename to src/fx_quant_engine/signals/confidence.py diff --git a/fx_quant_engine/signals/engine.py b/src/fx_quant_engine/signals/engine.py similarity index 100% rename from fx_quant_engine/signals/engine.py rename to src/fx_quant_engine/signals/engine.py diff --git a/fx_quant_engine/utils/__init__.py b/src/fx_quant_engine/utils/__init__.py similarity index 100% rename from fx_quant_engine/utils/__init__.py rename to src/fx_quant_engine/utils/__init__.py diff --git a/fx_quant_engine/utils/config.py b/src/fx_quant_engine/utils/config.py similarity index 100% rename from fx_quant_engine/utils/config.py rename to src/fx_quant_engine/utils/config.py diff --git a/fx_quant_engine/utils/logging.py b/src/fx_quant_engine/utils/logging.py similarity index 100% rename from fx_quant_engine/utils/logging.py rename to src/fx_quant_engine/utils/logging.py diff --git a/fx_quant_engine/utils/time.py b/src/fx_quant_engine/utils/time.py similarity index 100% rename from fx_quant_engine/utils/time.py rename to src/fx_quant_engine/utils/time.py