├── docs └── mkdocs.yml ├── envs ├── poetry.lock ├── conda.yml └── conda.yml.bak ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── .github ├── workflows │ ├── ci.yml │ ├── docs.yml │ └── release.yml ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── .devcontainer └── devcontainer.json ├── algos ├── etf_vol_breakout │ └── .gitkeep ├── crypto_momentum_rotation │ └── .gitkeep ├── position_unwinding │ ├── __init__.py │ ├── config.yaml │ ├── config.yaml.bak │ ├── example_usage.py │ ├── example_usage.py.bak │ ├── README.md │ ├── README.md.bak │ └── execution_analytics.py └── mean_reversion_sp100 │ ├── __init__.py │ ├── config.yaml │ ├── config.yaml.bak │ ├── strategy.py │ └── strategy.py.bak ├── quantdesk ├── math │ ├── __init__.py │ ├── garch.py │ ├── garch.py.bak │ ├── monte_carlo.py │ ├── monte_carlo.py.bak │ ├── kalman.py │ └── kalman.py.bak ├── __init__.py ├── api │ ├── schemas.py │ ├── schemas.py.bak │ ├── endpoints.py │ ├── endpoints.py.bak │ ├── broker_router.py │ └── broker_router.py.bak ├── utils │ ├── secrets.py │ ├── logging.py │ ├── logging.py.bak │ ├── env.py │ └── env.py.bak ├── research │ ├── stats_tests.py │ ├── stats_tests.py.bak │ ├── feature_engineering.py │ ├── feature_engineering.py.bak │ ├── model_selection.py │ └── model_selection.py.bak ├── core │ ├── risk.py │ ├── risk.py.bak │ ├── portfolio.py │ ├── portfolio.py.bak │ ├── event_engine.py │ ├── event_engine.py.bak │ ├── data_loader.py │ ├── data_loader.py.bak │ ├── metrics.py │ └── metrics.py.bak └── stratlib │ ├── utils.py │ ├── utils.py.bak │ ├── base_strategy.py │ └── base_strategy.py.bak ├── .gitignore ├── CONTRIBUTING.md ├── Dockerfile ├── docker-compose.yml ├── scripts ├── fetch_yf.py ├── fetch_yf.py.bak ├── ingest_alpaca.py ├── ingest_alpaca.py.bak ├── ingest_binance.py └── ingest_binance.py.bak ├── LICENSE ├── pyproject.toml ├── pyproject.toml.bak └── .env.template /docs/mkdocs.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /envs/poetry.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /algos/etf_vol_breakout/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /algos/crypto_momentum_rotation/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /quantdesk/math/__init__.py: -------------------------------------------------------------------------------- 1 | """Mathematical utilities for quantitative analysis.""" -------------------------------------------------------------------------------- /algos/position_unwinding/__init__.py: -------------------------------------------------------------------------------- 1 | """Position unwinding strategies for large institutional positions.""" -------------------------------------------------------------------------------- /algos/mean_reversion_sp100/__init__.py: -------------------------------------------------------------------------------- 1 | """Mean Reversion Strategy Package for S&P 100.""" 2 | from __future__ import annotations 3 | 4 | from .strategy import MeanReversionSP100 5 | 6 | __all__ = ["MeanReversionSP100"] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | *.so 5 | .Python 6 | build/ 7 | develop-eggs/ 8 | dist/ 9 | downloads/ 10 | eggs/ 11 | .eggs/ 12 | lib/ 13 | lib64/ 14 | parts/ 15 | sdist/ 16 | var/ 17 | wheels/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | MANIFEST 22 | 23 | .env 24 | .venv 25 | env/ 26 | venv/ 27 | ENV/ 28 | env.bak/ 29 | venv.bak/ 30 | 31 | .idea/ 32 | .vscode/ 33 | *.swp 34 | *.swo 35 | *~ 36 | 37 | data/cache/ 38 | *.log 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Quant Sentinel HiveMind Trading Fortress 2 | 3 | We welcome contributions to this quantitative trading platform. Please follow these guidelines: 4 | 5 | ## Development Setup 6 | 1. Clone the repository 7 | 2. Install dependencies: `pip install -e .` 8 | 3. Run tests: `pytest` 9 | 10 | ## Code Style 11 | - Follow PEP 8 12 | - Use type hints 13 | - Add docstrings to all functions 14 | - Use loguru for logging 15 | 16 | ## Pull Request Process 17 | 1. Fork the repository 18 | 2. Create a feature branch 19 | 3. Make your changes 20 | 4. Add tests 21 | 5. Submit a pull request 22 | -------------------------------------------------------------------------------- /quantdesk/__init__.py: -------------------------------------------------------------------------------- 1 | """QuantDesk: Open-source quantitative trading research platform.""" 2 | from __future__ import annotations 3 | 4 | __version__ = "0.1.0" 5 | __author__ = "QuantDesk Team" 6 | __email__ = "team@quantdesk.io" 7 | __license__ = "AGPL-3.0" 8 | 9 | # Core imports for easy access 10 | from quantdesk.core.data_loader import load 11 | from quantdesk.core.portfolio import Portfolio 12 | from quantdesk.core.event_engine import EventEngine 13 | from quantdesk.utils.logging import get_logger 14 | 15 | __all__ = [ 16 | "__version__", 17 | "load", 18 | "Portfolio", 19 | "EventEngine", 20 | "get_logger", 21 | ] -------------------------------------------------------------------------------- /quantdesk/api/schemas.py: -------------------------------------------------------------------------------- 1 | """Pydantic request/response models.""" 2 | from __future__ import annotations 3 | 4 | from datetime import datetime 5 | 6 | from pydantic import BaseModel, ConfigDict, Field 7 | 8 | 9 | class Order(BaseModel): 10 | model_config = ConfigDict(extra="forbid") 11 | 12 | symbol: str 13 | side: str = Field(pattern="^(buy|sell)$") 14 | qty: int 15 | order_type: str = Field(default="market", pattern="^(market|limit)$") 16 | limit_price: float | None = None 17 | timestamp: datetime | None = None 18 | 19 | 20 | class Position(BaseModel): 21 | symbol: str 22 | qty: int 23 | avg_price: float -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.5 2 | FROM python:3.11-slim 3 | 4 | ENV PYTHONDONTWRITEBYTECODE=1 \ 5 | PYTHONUNBUFFERED=1 \ 6 | POETRY_VERSION=1.8.2 7 | 8 | # System deps 9 | RUN apt-get update && apt-get install -y build-essential git && rm -rf /var/lib/apt/lists/* 10 | 11 | # Poetry + project 12 | RUN pip install --no-cache-dir poetry==$POETRY_VERSION 13 | WORKDIR /app 14 | COPY pyproject.toml poetry.lock /app/ 15 | RUN poetry install --no-dev --no-root --only main 16 | 17 | COPY quantdesk /app/quantdesk 18 | COPY scripts /app/scripts 19 | 20 | CMD ["uvicorn", "quantdesk.api.endpoints:app", "--host", "0.0.0.0", "--port", "8000"] -------------------------------------------------------------------------------- /quantdesk/api/schemas.py.bak: -------------------------------------------------------------------------------- 1 | """Pydantic request/response models.""" 2 | from __future__ import annotations 3 | 4 | from datetime import datetime 5 | 6 | from pydantic import BaseModel, ConfigDict, Field 7 | 8 | 9 | class Order(BaseModel): 10 | model_config = ConfigDict(extra="forbid") 11 | 12 | symbol: str 13 | side: str = Field(pattern="^(buy|sell)$") 14 | qty: int 15 | order_type: str = Field(default="market", pattern="^(market|limit)$") 16 | limit_price: float | None = None 17 | timestamp: datetime | None = None 18 | 19 | 20 | class Position(BaseModel): 21 | symbol: str 22 | qty: int 23 | avg_price: float -------------------------------------------------------------------------------- /quantdesk/math/garch.py: -------------------------------------------------------------------------------- 1 | """GARCH(1,1) volatility forecast.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | from arch import arch_model # optional dependency, pinned in conda/poetry 7 | 8 | 9 | def garch_vol(returns: pd.Series, horizon: int = 1) -> float: 10 | """Fit GARCH(1,1) model and forecast volatility. 11 | 12 | :param returns: Return series. 13 | :param horizon: Forecast horizon in periods. 14 | :return: Volatility forecast. 15 | """ 16 | model = arch_model(returns * 100, p=1, q=1) # % 17 | res = model.fit(disp="off") 18 | forecast = res.forecast(horizon=horizon) 19 | return float(np.sqrt(forecast.variance.iloc[-1, -1]) / 100) -------------------------------------------------------------------------------- /quantdesk/math/garch.py.bak: -------------------------------------------------------------------------------- 1 | """GARCH(1,1) volatility forecast.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | from arch import arch_model # optional dependency, pinned in conda/poetry 7 | 8 | 9 | def garch_vol(returns: pd.Series, horizon: int = 1) -> float: 10 | """Fit GARCH(1,1) model and forecast volatility. 11 | 12 | :param returns: Return series. 13 | :param horizon: Forecast horizon in periods. 14 | :return: Volatility forecast. 15 | """ 16 | model = arch_model(returns * 100, p=1, q=1) # % 17 | res = model.fit(disp="off") 18 | forecast = res.forecast(horizon=horizon) 19 | return float(np.sqrt(forecast.variance.iloc[-1, -1]) / 100) -------------------------------------------------------------------------------- /quantdesk/utils/secrets.py: -------------------------------------------------------------------------------- 1 | """Helpers for secret discovery inside containers or local OS keychains.""" 2 | from __future__ import annotations 3 | 4 | import os 5 | from base64 import b64decode 6 | from pathlib import Path 7 | 8 | _SECRET_DIR = Path("/run/secrets") # Docker Swarm / Compose secrets mount 9 | 10 | 11 | def get_secret(key: str, default: str | None = None) -> str: 12 | """Fetch secret value, prioritising Docker secrets over env.""" 13 | file_path = _SECRET_DIR / key 14 | if file_path.exists(): 15 | return file_path.read_text().strip() 16 | try: 17 | return os.environ[key] 18 | except KeyError as exc: 19 | if default is not None: 20 | return default 21 | raise RuntimeError(f"Missing secret: {key}") from exc -------------------------------------------------------------------------------- /quantdesk/research/stats_tests.py: -------------------------------------------------------------------------------- 1 | """Statistical hypothesis tests.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import statsmodels.api as sm 7 | from statsmodels.tsa.stattools import adfuller 8 | 9 | from quantdesk.utils.logging import get_logger 10 | 11 | log = get_logger(__name__) 12 | 13 | 14 | def ols(y: pd.Series, x: pd.Series) -> sm.regression.linear_model.RegressionResultsWrapper: 15 | x_const = sm.add_constant(x) 16 | model = sm.OLS(y, x_const).fit() 17 | return model 18 | 19 | 20 | def hurst(ts: pd.Series) -> float: 21 | lags = range(2, 100) 22 | log_rs = np.log([np.sqrt(((ts.diff(lag).dropna()) ** 2).mean()) for lag in lags]) 23 | log_lags = np.log(lags) 24 | slope, _ = np.polyfit(log_lags, log_rs, 1) 25 | return slope * 2.0 26 | 27 | 28 | def adf(ts: pd.Series) -> float: 29 | return adfuller(ts, maxlag=1, regression="c")[1] # p‑value -------------------------------------------------------------------------------- /quantdesk/research/stats_tests.py.bak: -------------------------------------------------------------------------------- 1 | """Statistical hypothesis tests.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import statsmodels.api as sm 7 | from statsmodels.tsa.stattools import adfuller 8 | 9 | from quantdesk.utils.logging import get_logger 10 | 11 | log = get_logger(__name__) 12 | 13 | 14 | def ols(y: pd.Series, x: pd.Series) -> sm.regression.linear_model.RegressionResultsWrapper: 15 | x_const = sm.add_constant(x) 16 | model = sm.OLS(y, x_const).fit() 17 | return model 18 | 19 | 20 | def hurst(ts: pd.Series) -> float: 21 | lags = range(2, 100) 22 | log_rs = np.log([np.sqrt(((ts.diff(lag).dropna()) ** 2).mean()) for lag in lags]) 23 | log_lags = np.log(lags) 24 | slope, _ = np.polyfit(log_lags, log_rs, 1) 25 | return slope * 2.0 26 | 27 | 28 | def adf(ts: pd.Series) -> float: 29 | return adfuller(ts, maxlag=1, regression="c")[1] # p‑value -------------------------------------------------------------------------------- /quantdesk/math/monte_carlo.py: -------------------------------------------------------------------------------- 1 | """Simple GBM Monte-Carlo simulator.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | def gbm_paths( 9 | s0: float, mu: float, sigma: float, days: int, n_paths: int = 1000 10 | ) -> pd.DataFrame: 11 | """Generate Geometric Brownian Motion price paths. 12 | 13 | :param s0: Initial price. 14 | :param mu: Drift parameter (annualized). 15 | :param sigma: Volatility parameter (annualized). 16 | :param days: Number of trading days to simulate. 17 | :param n_paths: Number of simulation paths. 18 | :return: DataFrame with price paths indexed by business days. 19 | """ 20 | dt = 1 / 252 21 | shocks = np.random.normal(mu * dt, sigma * np.sqrt(dt), size=(days, n_paths)) 22 | price = s0 * np.exp(shocks.cumsum(axis=0)) 23 | idx = pd.date_range("today", periods=days, freq="B") 24 | return pd.DataFrame(price, index=idx) -------------------------------------------------------------------------------- /quantdesk/math/monte_carlo.py.bak: -------------------------------------------------------------------------------- 1 | """Simple GBM Monte-Carlo simulator.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | def gbm_paths( 9 | s0: float, mu: float, sigma: float, days: int, n_paths: int = 1000 10 | ) -> pd.DataFrame: 11 | """Generate Geometric Brownian Motion price paths. 12 | 13 | :param s0: Initial price. 14 | :param mu: Drift parameter (annualized). 15 | :param sigma: Volatility parameter (annualized). 16 | :param days: Number of trading days to simulate. 17 | :param n_paths: Number of simulation paths. 18 | :return: DataFrame with price paths indexed by business days. 19 | """ 20 | dt = 1 / 252 21 | shocks = np.random.normal(mu * dt, sigma * np.sqrt(dt), size=(days, n_paths)) 22 | price = s0 * np.exp(shocks.cumsum(axis=0)) 23 | idx = pd.date_range("today", periods=days, freq="B") 24 | return pd.DataFrame(price, index=idx) -------------------------------------------------------------------------------- /quantdesk/research/feature_engineering.py: -------------------------------------------------------------------------------- 1 | """TA features and PCA factor reduction.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import ta # ta‑lib wrapper 7 | 8 | from sklearn.decomposition import PCA 9 | 10 | 11 | def ta_features(df: pd.DataFrame) -> pd.DataFrame: 12 | out = pd.DataFrame(index=df.index) 13 | out["rsi_14"] = ta.momentum.rsi(df["close"], window=14) 14 | out["macd"] = ta.trend.macd_diff(df["close"]) 15 | out["atr"] = ta.volatility.average_true_range(df["high"], df["low"], df["close"]) 16 | return out.fillna(method="bfill") 17 | 18 | 19 | def pca_factors(features: pd.DataFrame, n_components: int = 5) -> pd.DataFrame: 20 | pca = PCA(n_components=n_components, whiten=True, svd_solver="full") 21 | comps = pca.fit_transform(features) 22 | cols = [f"pca_{i}" for i in range(comps.shape[1])] 23 | return pd.DataFrame(comps, index=features.index, columns=cols) -------------------------------------------------------------------------------- /quantdesk/research/feature_engineering.py.bak: -------------------------------------------------------------------------------- 1 | """TA features and PCA factor reduction.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import ta # ta‑lib wrapper 7 | 8 | from sklearn.decomposition import PCA 9 | 10 | 11 | def ta_features(df: pd.DataFrame) -> pd.DataFrame: 12 | out = pd.DataFrame(index=df.index) 13 | out["rsi_14"] = ta.momentum.rsi(df["close"], window=14) 14 | out["macd"] = ta.trend.macd_diff(df["close"]) 15 | out["atr"] = ta.volatility.average_true_range(df["high"], df["low"], df["close"]) 16 | return out.fillna(method="bfill") 17 | 18 | 19 | def pca_factors(features: pd.DataFrame, n_components: int = 5) -> pd.DataFrame: 20 | pca = PCA(n_components=n_components, whiten=True, svd_solver="full") 21 | comps = pca.fit_transform(features) 22 | cols = [f"pca_{i}" for i in range(comps.shape[1])] 23 | return pd.DataFrame(comps, index=features.index, columns=cols) -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | api: 4 | build: . 5 | env_file: 6 | - .env 7 | ports: 8 | - "8000:8000" 9 | container_name: quantdesk-api 10 | restart: unless-stopped 11 | healthcheck: 12 | test: ["CMD", "curl", "-f", "http://localhost:8000/health"] 13 | interval: 30s 14 | timeout: 5s 15 | retries: 3 16 | depends_on: 17 | - redis 18 | - mlflow 19 | volumes: 20 | - ./data:/app/data 21 | - ./models:/app/models 22 | - ./logs:/app/logs 23 | redis: 24 | image: redis:7-alpine 25 | container_name: quantdesk-redis 26 | restart: unless-stopped 27 | volumes: 28 | - redis-data:/data 29 | mlflow: 30 | image: ghcr.io/mlflow/mlflow:v2.14.1 31 | ports: 32 | - "5000:5000" 33 | container_name: quantdesk-mlflow 34 | restart: unless-stopped 35 | volumes: 36 | - mlruns:/mlruns 37 | 38 | volumes: 39 | redis-data: 40 | mlruns: -------------------------------------------------------------------------------- /quantdesk/utils/logging.py: -------------------------------------------------------------------------------- 1 | """Structured JSON logging.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | import sys 6 | from typing import Any 7 | 8 | import structlog 9 | 10 | LOG_LEVEL = logging.INFO 11 | 12 | _console = logging.StreamHandler(sys.stdout) 13 | logging.basicConfig( 14 | handlers=[_console], level=LOG_LEVEL, format="%(message)s", force=True 15 | ) 16 | 17 | structlog.configure( 18 | wrapper_class=structlog.make_filtering_bound_logger(LOG_LEVEL), 19 | logger_factory=structlog.stdlib.LoggerFactory(), 20 | processors=[ 21 | structlog.processors.TimeStamper(fmt="iso"), 22 | structlog.processors.add_log_level, 23 | structlog.processors.StackInfoRenderer(), 24 | structlog.processors.format_exc_info, 25 | structlog.processors.JSONRenderer(), 26 | ], 27 | ) 28 | 29 | 30 | def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger[Any]: 31 | return structlog.get_logger(name or "quantdesk") -------------------------------------------------------------------------------- /quantdesk/utils/logging.py.bak: -------------------------------------------------------------------------------- 1 | """Structured JSON logging.""" 2 | from __future__ import annotations 3 | 4 | import logging 5 | import sys 6 | from typing import Any 7 | 8 | import structlog 9 | 10 | LOG_LEVEL = logging.INFO 11 | 12 | _console = logging.StreamHandler(sys.stdout) 13 | logging.basicConfig( 14 | handlers=[_console], level=LOG_LEVEL, format="%(message)s", force=True 15 | ) 16 | 17 | structlog.configure( 18 | wrapper_class=structlog.make_filtering_bound_logger(LOG_LEVEL), 19 | logger_factory=structlog.stdlib.LoggerFactory(), 20 | processors=[ 21 | structlog.processors.TimeStamper(fmt="iso"), 22 | structlog.processors.add_log_level, 23 | structlog.processors.StackInfoRenderer(), 24 | structlog.processors.format_exc_info, 25 | structlog.processors.JSONRenderer(), 26 | ], 27 | ) 28 | 29 | 30 | def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger[Any]: 31 | return structlog.get_logger(name or "quantdesk") -------------------------------------------------------------------------------- /scripts/fetch_yf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """CLI: Bulk‑download Yahoo Finance data.""" 3 | from __future__ import annotations 4 | 5 | import click 6 | from datetime import datetime 7 | import pandas as pd 8 | from pathlib import Path 9 | 10 | from quantdesk.core.data_loader import load 11 | 12 | 13 | @click.command() 14 | @click.argument("symbol") 15 | @click.option("--start", default="2010‑01‑01") 16 | @click.option("--end", default=datetime.utcnow().strftime("%Y‑%m‑%d")) 17 | @click.option("--freq", default="1d") 18 | @click.option("--outdir", default="data/cache") 19 | def main(symbol: str, start: str, end: str, freq: str, outdir: str) -> None: 20 | df = load(symbol, datetime.fromisoformat(start), datetime.fromisoformat(end), freq=freq) 21 | out_path = Path(outdir) / "yahoo" / symbol / f"{freq}.parquet" 22 | out_path.parent.mkdir(parents=True, exist_ok=True) 23 | df.to_parquet(out_path) 24 | click.echo(f"Saved {len(df)} rows → {out_path}") 25 | 26 | 27 | if __name__ == "__main__": 28 | main() -------------------------------------------------------------------------------- /scripts/fetch_yf.py.bak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """CLI: Bulk‑download Yahoo Finance data.""" 3 | from __future__ import annotations 4 | 5 | import click 6 | from datetime import datetime 7 | import pandas as pd 8 | from pathlib import Path 9 | 10 | from quantdesk.core.data_loader import load 11 | 12 | 13 | @click.command() 14 | @click.argument("symbol") 15 | @click.option("--start", default="2010‑01‑01") 16 | @click.option("--end", default=datetime.utcnow().strftime("%Y‑%m‑%d")) 17 | @click.option("--freq", default="1d") 18 | @click.option("--outdir", default="data/cache") 19 | def main(symbol: str, start: str, end: str, freq: str, outdir: str) -> None: 20 | df = load(symbol, datetime.fromisoformat(start), datetime.fromisoformat(end), freq=freq) 21 | out_path = Path(outdir) / "yahoo" / symbol / f"{freq}.parquet" 22 | out_path.parent.mkdir(parents=True, exist_ok=True) 23 | df.to_parquet(out_path) 24 | click.echo(f"Saved {len(df)} rows → {out_path}") 25 | 26 | 27 | if __name__ == "__main__": 28 | main() -------------------------------------------------------------------------------- /quantdesk/core/risk.py: -------------------------------------------------------------------------------- 1 | """Position sizing and risk calculations.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | from scipy.stats import norm 7 | 8 | from quantdesk.utils.logging import get_logger 9 | 10 | log = get_logger(__name__) 11 | 12 | 13 | def kelly_fraction(edge: float, variance: float) -> float: 14 | """Kelly % for given edge and variance.""" 15 | k = edge / variance 16 | return max(0.0, min(k, 1.0)) # cap at 100 % 17 | 18 | 19 | def cvar(returns: pd.Series, level: float = 0.95) -> float: 20 | """Conditional Value‑at‑Risk (expected shortfall).""" 21 | var = returns.quantile(1 - level) 22 | return returns[returns <= var].mean() 23 | 24 | 25 | def volatility_target(position_value: float, annual_vol_target: float, vol_est: float) -> float: 26 | """Target dollar allocation given vol forecast.""" 27 | if vol_est == 0: 28 | return 0 29 | target = position_value * (annual_vol_target / vol_est) 30 | log.debug("risk.vol_target", target=target, vol_est=vol_est) 31 | return target -------------------------------------------------------------------------------- /quantdesk/core/risk.py.bak: -------------------------------------------------------------------------------- 1 | """Position sizing and risk calculations.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | from scipy.stats import norm 7 | 8 | from quantdesk.utils.logging import get_logger 9 | 10 | log = get_logger(__name__) 11 | 12 | 13 | def kelly_fraction(edge: float, variance: float) -> float: 14 | """Kelly % for given edge and variance.""" 15 | k = edge / variance 16 | return max(0.0, min(k, 1.0)) # cap at 100 % 17 | 18 | 19 | def cvar(returns: pd.Series, level: float = 0.95) -> float: 20 | """Conditional Value‑at‑Risk (expected shortfall).""" 21 | var = returns.quantile(1 - level) 22 | return returns[returns <= var].mean() 23 | 24 | 25 | def volatility_target(position_value: float, annual_vol_target: float, vol_est: float) -> float: 26 | """Target dollar allocation given vol forecast.""" 27 | if vol_est == 0: 28 | return 0 29 | target = position_value * (annual_vol_target / vol_est) 30 | log.debug("risk.vol_target", target=target, vol_est=vol_est) 31 | return target -------------------------------------------------------------------------------- /quantdesk/math/kalman.py: -------------------------------------------------------------------------------- 1 | """1-dimensional Kalman filter for time-varying hedge ratio.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | def kalman_filter(y: pd.Series, x: pd.Series) -> pd.Series: 9 | """Apply 1-dimensional Kalman filter to estimate time-varying hedge ratio. 10 | 11 | :param y: Dependent variable series. 12 | :param x: Independent variable series. 13 | :return: Time-varying beta estimates. 14 | """ 15 | n = len(y) 16 | delta = 1e-5 17 | vt, rt, qt, at, bt = (np.zeros(n) for _ in range(5)) 18 | Pt = np.zeros(n) 19 | beta = 0.0 20 | P = 1.0 21 | 22 | for t in range(n): 23 | at[t] = beta 24 | rt[t] = P + delta 25 | vt[t] = y.iloc[t] - at[t] * x.iloc[t] 26 | qt[t] = rt[t] * x.iloc[t] ** 2 + 1.0 # obs noise σ² = 1 27 | bt[t] = at[t] + rt[t] * x.iloc[t] * vt[t] / qt[t] 28 | Pt = rt[t] - rt[t] ** 2 * x.iloc[t] ** 2 / qt[t] 29 | beta = bt[t] 30 | P = Pt 31 | return pd.Series(bt, index=y.index, name="kalman_beta") -------------------------------------------------------------------------------- /quantdesk/math/kalman.py.bak: -------------------------------------------------------------------------------- 1 | """1-dimensional Kalman filter for time-varying hedge ratio.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | def kalman_filter(y: pd.Series, x: pd.Series) -> pd.Series: 9 | """Apply 1-dimensional Kalman filter to estimate time-varying hedge ratio. 10 | 11 | :param y: Dependent variable series. 12 | :param x: Independent variable series. 13 | :return: Time-varying beta estimates. 14 | """ 15 | n = len(y) 16 | delta = 1e-5 17 | vt, rt, qt, at, bt = (np.zeros(n) for _ in range(5)) 18 | Pt = np.zeros(n) 19 | beta = 0.0 20 | P = 1.0 21 | 22 | for t in range(n): 23 | at[t] = beta 24 | rt[t] = P + delta 25 | vt[t] = y.iloc[t] - at[t] * x.iloc[t] 26 | qt[t] = rt[t] * x.iloc[t] ** 2 + 1.0 # obs noise σ² = 1 27 | bt[t] = at[t] + rt[t] * x.iloc[t] * vt[t] / qt[t] 28 | Pt = rt[t] - rt[t] ** 2 * x.iloc[t] ** 2 / qt[t] 29 | beta = bt[t] 30 | P = Pt 31 | return pd.Series(bt, index=y.index, name="kalman_beta") -------------------------------------------------------------------------------- /quantdesk/utils/env.py: -------------------------------------------------------------------------------- 1 | """Environment variable loader & validator.""" 2 | from __future__ import annotations 3 | 4 | import os 5 | from pathlib import Path 6 | 7 | from pydantic import BaseModel, Field, ValidationError 8 | from dotenv import load_dotenv 9 | 10 | _ENV_PATH = Path(__file__).resolve().parents[2] / ".env" 11 | load_dotenv(_ENV_PATH, override=False) 12 | 13 | 14 | class Settings(BaseModel): 15 | """Runtime configuration pulled from the environment.""" 16 | 17 | alpaca_key: str = Field(..., env="ALPACA_KEY_ID") 18 | alpaca_secret: str = Field(..., env="ALPACA_SECRET_KEY") 19 | binance_key: str = Field(..., env="BINANCE_API_KEY") 20 | binance_secret: str = Field(..., env="BINANCE_API_SECRET") 21 | discord_webhook: str = Field(..., env="DISCORD_WEBHOOK_URL") 22 | mlflow_uri: str = Field("http://mlflow:5000", env="MLFLOW_TRACKING_URI") 23 | 24 | class Config: 25 | extra = "forbid" 26 | frozen = True 27 | 28 | 29 | try: 30 | SETTINGS = Settings() # validated at import‑time 31 | except ValidationError as e: # pragma: no cover 32 | raise RuntimeError(f"Invalid .env ‑ {e}") from e -------------------------------------------------------------------------------- /quantdesk/utils/env.py.bak: -------------------------------------------------------------------------------- 1 | """Environment variable loader & validator.""" 2 | from __future__ import annotations 3 | 4 | import os 5 | from pathlib import Path 6 | 7 | from pydantic import BaseModel, Field, ValidationError 8 | from dotenv import load_dotenv 9 | 10 | _ENV_PATH = Path(__file__).resolve().parents[2] / ".env" 11 | load_dotenv(_ENV_PATH, override=False) 12 | 13 | 14 | class Settings(BaseModel): 15 | """Runtime configuration pulled from the environment.""" 16 | 17 | alpaca_key: str = Field(..., env="ALPACA_KEY_ID") 18 | alpaca_secret: str = Field(..., env="ALPACA_SECRET_KEY") 19 | binance_key: str = Field(..., env="BINANCE_API_KEY") 20 | binance_secret: str = Field(..., env="BINANCE_API_SECRET") 21 | discord_webhook: str = Field(..., env="DISCORD_WEBHOOK_URL") 22 | mlflow_uri: str = Field("http://mlflow:5000", env="MLFLOW_TRACKING_URI") 23 | 24 | class Config: 25 | extra = "forbid" 26 | frozen = True 27 | 28 | 29 | try: 30 | SETTINGS = Settings() # validated at import‑time 31 | except ValidationError as e: # pragma: no cover 32 | raise RuntimeError(f"Invalid .env ‑ {e}") from e -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Quant Sentinel HiveMind 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /quantdesk/research/model_selection.py: -------------------------------------------------------------------------------- 1 | """Walk‑forward cross‑validation & Bayesian optimisation.""" 2 | from __future__ import annotations 3 | 4 | import itertools 5 | from datetime import timedelta 6 | from typing import Callable 7 | 8 | import mlflow 9 | import numpy as np 10 | import pandas as pd 11 | from skopt import gp_minimize 12 | from skopt.space import Real, Integer 13 | 14 | from quantdesk.utils.logging import get_logger 15 | 16 | log = get_logger(__name__) 17 | 18 | 19 | def walk_forward_splits( 20 | df: pd.DataFrame, train_size: int, test_size: int 21 | ) -> list[tuple[pd.DatetimeIndex, pd.DatetimeIndex]]: 22 | """Return list of index splits (train_idx, test_idx).""" 23 | splits = [] 24 | start = 0 25 | while start + train_size + test_size <= len(df): 26 | train_idx = df.index[start : start + train_size] 27 | test_idx = df.index[start + train_size : start + train_size + test_size] 28 | splits.append((train_idx, test_idx)) 29 | start += test_size 30 | return splits 31 | 32 | 33 | def bayes_opt( 34 | obj: Callable[[dict[str, float]], float], 35 | space: list[Real | Integer], 36 | n_calls: int = 30, 37 | ) -> dict[str, float]: 38 | res = gp_minimize(obj, space, n_calls=n_calls, random_state=42) 39 | return {f"x{i}": v for i, v in enumerate(res.x)} -------------------------------------------------------------------------------- /quantdesk/research/model_selection.py.bak: -------------------------------------------------------------------------------- 1 | """Walk‑forward cross‑validation & Bayesian optimisation.""" 2 | from __future__ import annotations 3 | 4 | import itertools 5 | from datetime import timedelta 6 | from typing import Callable 7 | 8 | import mlflow 9 | import numpy as np 10 | import pandas as pd 11 | from skopt import gp_minimize 12 | from skopt.space import Real, Integer 13 | 14 | from quantdesk.utils.logging import get_logger 15 | 16 | log = get_logger(__name__) 17 | 18 | 19 | def walk_forward_splits( 20 | df: pd.DataFrame, train_size: int, test_size: int 21 | ) -> list[tuple[pd.DatetimeIndex, pd.DatetimeIndex]]: 22 | """Return list of index splits (train_idx, test_idx).""" 23 | splits = [] 24 | start = 0 25 | while start + train_size + test_size <= len(df): 26 | train_idx = df.index[start : start + train_size] 27 | test_idx = df.index[start + train_size : start + train_size + test_size] 28 | splits.append((train_idx, test_idx)) 29 | start += test_size 30 | return splits 31 | 32 | 33 | def bayes_opt( 34 | obj: Callable[[dict[str, float]], float], 35 | space: list[Real | Integer], 36 | n_calls: int = 30, 37 | ) -> dict[str, float]: 38 | res = gp_minimize(obj, space, n_calls=n_calls, random_state=42) 39 | return {f"x{i}": v for i, v in enumerate(res.x)} -------------------------------------------------------------------------------- /quantdesk/stratlib/utils.py: -------------------------------------------------------------------------------- 1 | """Reusable helpers such as ATR stops & sizing.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | def atr_stop(price: pd.Series, atr: pd.Series, mult: float = 2.0) -> pd.Series: 9 | return price - mult * atr 10 | 11 | 12 | def position_size( 13 | capital: float, vol_target: float, vol_est: float, price: float 14 | ) -> int: 15 | dollar_qty = capital * (vol_target / max(vol_est, 1e-8)) 16 | return int(dollar_qty // price) 17 | 18 | 19 | def compute_atr( 20 | high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14 21 | ) -> pd.Series: 22 | """Average True Range calculated purely with pandas/NumPy. 23 | 24 | :param high: High price series. 25 | :param low: Low price series. 26 | :param close: Close price series. 27 | :param period: Rolling window length. 28 | :return: ATR series. 29 | """ 30 | prev_close = close.shift(1) 31 | tr = np.maximum(high - low, np.maximum((high - prev_close).abs(), (low - prev_close).abs())) 32 | return tr.rolling(period, min_periods=1).mean() 33 | 34 | 35 | def rolling_volatility(returns: pd.Series, window: int = 20) -> pd.Series: 36 | """Annualised rolling volatility (σ) based on daily returns. 37 | 38 | :param returns: Daily return series. 39 | :param window: Look-back window length. 40 | :return: Annualised rolling volatility. 41 | """ 42 | return returns.rolling(window).std(ddof=0) * np.sqrt(252) -------------------------------------------------------------------------------- /quantdesk/stratlib/utils.py.bak: -------------------------------------------------------------------------------- 1 | """Reusable helpers such as ATR stops & sizing.""" 2 | from __future__ import annotations 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | 8 | def atr_stop(price: pd.Series, atr: pd.Series, mult: float = 2.0) -> pd.Series: 9 | return price - mult * atr 10 | 11 | 12 | def position_size( 13 | capital: float, vol_target: float, vol_est: float, price: float 14 | ) -> int: 15 | dollar_qty = capital * (vol_target / max(vol_est, 1e-8)) 16 | return int(dollar_qty // price) 17 | 18 | 19 | def compute_atr( 20 | high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14 21 | ) -> pd.Series: 22 | """Average True Range calculated purely with pandas/NumPy. 23 | 24 | :param high: High price series. 25 | :param low: Low price series. 26 | :param close: Close price series. 27 | :param period: Rolling window length. 28 | :return: ATR series. 29 | """ 30 | prev_close = close.shift(1) 31 | tr = np.maximum(high - low, np.maximum((high - prev_close).abs(), (low - prev_close).abs())) 32 | return tr.rolling(period, min_periods=1).mean() 33 | 34 | 35 | def rolling_volatility(returns: pd.Series, window: int = 20) -> pd.Series: 36 | """Annualised rolling volatility (σ) based on daily returns. 37 | 38 | :param returns: Daily return series. 39 | :param window: Look-back window length. 40 | :return: Annualised rolling volatility. 41 | """ 42 | return returns.rolling(window).std(ddof=0) * np.sqrt(252) -------------------------------------------------------------------------------- /quantdesk/api/endpoints.py: -------------------------------------------------------------------------------- 1 | """FastAPI service.""" 2 | from __future__ import annotations 3 | 4 | from fastapi import FastAPI, WebSocket, WebSocketDisconnect 5 | from quantdesk.api.schemas import Order, Position 6 | from quantdesk.api.broker_router import BrokerRouter 7 | from quantdesk.core.portfolio import Portfolio 8 | 9 | app = FastAPI(title="QuantDesk API") 10 | router = BrokerRouter() 11 | PORTFOLIO = Portfolio(cash=1_000_000.0) 12 | 13 | 14 | @app.post("/orders") 15 | async def submit_order(order: Order) -> dict[str, str]: 16 | order_id = await router.route(order) 17 | return {"order_id": order_id} 18 | 19 | 20 | @app.get("/positions") 21 | async def positions() -> list[Position]: 22 | mark = {} # could hit real‑time data 23 | df = PORTFOLIO.to_dataframe(mark) 24 | return df.to_dict(orient="records") 25 | 26 | 27 | @app.websocket("/ws/fills") 28 | async def stream_fills(ws: WebSocket) -> None: # simplistic demo 29 | await ws.accept() 30 | try: 31 | while True: 32 | await ws.send_json({"msg": "heartbeat"}) 33 | await ws.receive_text() 34 | except WebSocketDisconnect: 35 | return 36 | 37 | 38 | @app.get("/health", include_in_schema=False) 39 | async def health() -> dict[str, str]: 40 | """Basic liveness probe used by orchestrators.""" 41 | return {"status": "ok"} 42 | 43 | 44 | @app.get("/portfolio/value") 45 | async def portfolio_value() -> dict[str, float]: 46 | """Return current portfolio gross value (cash + equity).""" 47 | mark = {} 48 | return {"value": PORTFOLIO.value(mark)} -------------------------------------------------------------------------------- /quantdesk/api/endpoints.py.bak: -------------------------------------------------------------------------------- 1 | """FastAPI service.""" 2 | from __future__ import annotations 3 | 4 | from fastapi import FastAPI, WebSocket, WebSocketDisconnect 5 | from quantdesk.api.schemas import Order, Position 6 | from quantdesk.api.broker_router import BrokerRouter 7 | from quantdesk.core.portfolio import Portfolio 8 | 9 | app = FastAPI(title="QuantDesk API") 10 | router = BrokerRouter() 11 | PORTFOLIO = Portfolio(cash=1_000_000.0) 12 | 13 | 14 | @app.post("/orders") 15 | async def submit_order(order: Order) -> dict[str, str]: 16 | order_id = await router.route(order) 17 | return {"order_id": order_id} 18 | 19 | 20 | @app.get("/positions") 21 | async def positions() -> list[Position]: 22 | mark = {} # could hit real‑time data 23 | df = PORTFOLIO.to_dataframe(mark) 24 | return df.to_dict(orient="records") 25 | 26 | 27 | @app.websocket("/ws/fills") 28 | async def stream_fills(ws: WebSocket) -> None: # simplistic demo 29 | await ws.accept() 30 | try: 31 | while True: 32 | await ws.send_json({"msg": "heartbeat"}) 33 | await ws.receive_text() 34 | except WebSocketDisconnect: 35 | return 36 | 37 | 38 | @app.get("/health", include_in_schema=False) 39 | async def health() -> dict[str, str]: 40 | """Basic liveness probe used by orchestrators.""" 41 | return {"status": "ok"} 42 | 43 | 44 | @app.get("/portfolio/value") 45 | async def portfolio_value() -> dict[str, float]: 46 | """Return current portfolio gross value (cash + equity).""" 47 | mark = {} 48 | return {"value": PORTFOLIO.value(mark)} -------------------------------------------------------------------------------- /envs/conda.yml: -------------------------------------------------------------------------------- 1 | name: quantdesk 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python=3.11 7 | - pip 8 | - numpy=1.27.0 9 | - pandas=2.2.2 10 | - scipy=1.13.0 11 | - scikit-learn=1.5.0 12 | - matplotlib=3.9.1 13 | - seaborn=0.13.2 14 | - plotly=5.22.0 15 | - jupyter 16 | - jupyterlab=4.2.3 17 | - ipywidgets=8.1.3 18 | - notebook 19 | - redis-py=5.0.7 20 | - psycopg2=2.9.9 21 | - sqlalchemy=2.0.31 22 | - click=8.1.7 23 | - pydantic=2.8.1 24 | - fastapi=0.111.0 25 | - uvicorn=0.30.0 26 | - aiohttp=3.9.5 27 | - websockets=12.0 28 | - python-dotenv=1.0.1 29 | - pyarrow=16.1.0 30 | - git 31 | - curl 32 | - pip: 33 | - ta==0.10.2 34 | - backtrader==1.9.78.123 35 | - vectorbt==0.26.0 36 | - mlflow==2.14.1 37 | - torch==2.3.0 38 | - yfinance==0.2.40 39 | - alpaca-trade-api==3.0.0 40 | - ccxt==4.3.37 41 | - structlog==24.1.0 42 | - scikit-optimize==0.9.0 43 | - arch==6.3.0 44 | - quantstats==0.0.62 45 | - alembic==1.13.2 46 | - apscheduler==3.10.4 47 | - prometheus-client==0.20.0 48 | - discord-webhook==1.3.1 49 | - fastavro==1.9.4 50 | - statsmodels==0.15.0 51 | - ruff==0.4.5 52 | - black==24.3.0 53 | - mypy==1.10.0 54 | - pytest==8.2.0 55 | - pytest-cov==5.0.0 56 | - pytest-xdist==3.6.0 57 | - pytest-asyncio==0.23.7 58 | - bandit==1.7.9 59 | - pre-commit==3.7.1 60 | - mkdocs-material==9.5.18 61 | - mkdocstrings[python]==0.25.0 62 | - jupyter-book==1.0.0 63 | - pdoc==14.5.1 64 | - httpx==0.27.0 65 | - pytest-mock==3.14.0 66 | - factory-boy==3.3.0 67 | - freezegun==1.5.1 -------------------------------------------------------------------------------- /envs/conda.yml.bak: -------------------------------------------------------------------------------- 1 | name: quantdesk 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python=3.11 7 | - pip 8 | - numpy=1.27.0 9 | - pandas=2.2.2 10 | - scipy=1.13.0 11 | - scikit-learn=1.5.0 12 | - matplotlib=3.9.1 13 | - seaborn=0.13.2 14 | - plotly=5.22.0 15 | - jupyter 16 | - jupyterlab=4.2.3 17 | - ipywidgets=8.1.3 18 | - notebook 19 | - redis-py=5.0.7 20 | - psycopg2=2.9.9 21 | - sqlalchemy=2.0.31 22 | - click=8.1.7 23 | - pydantic=2.8.1 24 | - fastapi=0.111.0 25 | - uvicorn=0.30.0 26 | - aiohttp=3.9.5 27 | - websockets=12.0 28 | - python-dotenv=1.0.1 29 | - pyarrow=16.1.0 30 | - git 31 | - curl 32 | - pip: 33 | - ta==0.10.2 34 | - backtrader==1.9.78.123 35 | - vectorbt==0.26.0 36 | - mlflow==2.14.1 37 | - torch==2.3.0 38 | - yfinance==0.2.40 39 | - alpaca-trade-api==3.0.0 40 | - ccxt==4.3.37 41 | - structlog==24.1.0 42 | - scikit-optimize==0.9.0 43 | - arch==6.3.0 44 | - quantstats==0.0.62 45 | - alembic==1.13.2 46 | - apscheduler==3.10.4 47 | - prometheus-client==0.20.0 48 | - discord-webhook==1.3.1 49 | - fastavro==1.9.4 50 | - statsmodels==0.15.0 51 | - ruff==0.4.5 52 | - black==24.3.0 53 | - mypy==1.10.0 54 | - pytest==8.2.0 55 | - pytest-cov==5.0.0 56 | - pytest-xdist==3.6.0 57 | - pytest-asyncio==0.23.7 58 | - bandit==1.7.9 59 | - pre-commit==3.7.1 60 | - mkdocs-material==9.5.18 61 | - mkdocstrings[python]==0.25.0 62 | - jupyter-book==1.0.0 63 | - pdoc==14.5.1 64 | - httpx==0.27.0 65 | - pytest-mock==3.14.0 66 | - factory-boy==3.3.0 67 | - freezegun==1.5.1 -------------------------------------------------------------------------------- /quantdesk/core/portfolio.py: -------------------------------------------------------------------------------- 1 | """Cash & position ledger with vectorised analytics.""" 2 | from __future__ import annotations 3 | 4 | from dataclasses import dataclass, field 5 | from typing import Dict 6 | 7 | import pandas as pd 8 | 9 | from quantdesk.core.event_engine import FillEvent 10 | from quantdesk.utils.logging import get_logger 11 | 12 | log = get_logger(__name__) 13 | 14 | 15 | @dataclass(slots=True) 16 | class Position: 17 | qty: int = 0 18 | avg_price: float = 0.0 19 | 20 | def update(self, fill_price: float, quantity: int) -> None: 21 | total_cost = self.avg_price * self.qty + fill_price * quantity 22 | self.qty += quantity 23 | self.avg_price = 0.0 if self.qty == 0 else total_cost / self.qty 24 | 25 | 26 | @dataclass 27 | class Portfolio: 28 | cash: float 29 | positions: Dict[str, Position] = field(default_factory=dict) 30 | 31 | def update_fill(self, fill: FillEvent) -> None: 32 | symbol = fill.symbol 33 | pos = self.positions.setdefault(symbol, Position()) 34 | pos.update(fill.fill_price, fill.direction * fill.quantity) 35 | cost = fill.fill_price * fill.quantity + fill.commission 36 | self.cash -= cost 37 | log.info( 38 | "portfolio.fill", 39 | symbol=symbol, 40 | qty=pos.qty, 41 | cash=self.cash, 42 | cost=cost, 43 | ) 44 | 45 | def value(self, mark: dict[str, float]) -> float: 46 | equity = sum(pos.qty * mark.get(sym, 0.0) for sym, pos in self.positions.items()) 47 | return self.cash + equity 48 | 49 | def to_dataframe(self, mark: dict[str, float]) -> pd.DataFrame: 50 | rows = [] 51 | for sym, pos in self.positions.items(): 52 | market = mark.get(sym, 0.0) 53 | rows.append( 54 | { 55 | "symbol": sym, 56 | "qty": pos.qty, 57 | "avg_price": pos.avg_price, 58 | "market_price": market, 59 | "unrealised_pnl": (market - pos.avg_price) * pos.qty, 60 | } 61 | ) 62 | return pd.DataFrame(rows) -------------------------------------------------------------------------------- /quantdesk/core/portfolio.py.bak: -------------------------------------------------------------------------------- 1 | """Cash & position ledger with vectorised analytics.""" 2 | from __future__ import annotations 3 | 4 | from dataclasses import dataclass, field 5 | from typing import Dict 6 | 7 | import pandas as pd 8 | 9 | from quantdesk.core.event_engine import FillEvent 10 | from quantdesk.utils.logging import get_logger 11 | 12 | log = get_logger(__name__) 13 | 14 | 15 | @dataclass(slots=True) 16 | class Position: 17 | qty: int = 0 18 | avg_price: float = 0.0 19 | 20 | def update(self, fill_price: float, quantity: int) -> None: 21 | total_cost = self.avg_price * self.qty + fill_price * quantity 22 | self.qty += quantity 23 | self.avg_price = 0.0 if self.qty == 0 else total_cost / self.qty 24 | 25 | 26 | @dataclass 27 | class Portfolio: 28 | cash: float 29 | positions: Dict[str, Position] = field(default_factory=dict) 30 | 31 | def update_fill(self, fill: FillEvent) -> None: 32 | symbol = fill.symbol 33 | pos = self.positions.setdefault(symbol, Position()) 34 | pos.update(fill.fill_price, fill.direction * fill.quantity) 35 | cost = fill.fill_price * fill.quantity + fill.commission 36 | self.cash -= cost 37 | log.info( 38 | "portfolio.fill", 39 | symbol=symbol, 40 | qty=pos.qty, 41 | cash=self.cash, 42 | cost=cost, 43 | ) 44 | 45 | def value(self, mark: dict[str, float]) -> float: 46 | equity = sum(pos.qty * mark.get(sym, 0.0) for sym, pos in self.positions.items()) 47 | return self.cash + equity 48 | 49 | def to_dataframe(self, mark: dict[str, float]) -> pd.DataFrame: 50 | rows = [] 51 | for sym, pos in self.positions.items(): 52 | market = mark.get(sym, 0.0) 53 | rows.append( 54 | { 55 | "symbol": sym, 56 | "qty": pos.qty, 57 | "avg_price": pos.avg_price, 58 | "market_price": market, 59 | "unrealised_pnl": (market - pos.avg_price) * pos.qty, 60 | } 61 | ) 62 | return pd.DataFrame(rows) -------------------------------------------------------------------------------- /quantdesk/core/event_engine.py: -------------------------------------------------------------------------------- 1 | """Simple event queue driving Backtrader + live trading.""" 2 | from __future__ import annotations 3 | 4 | import queue 5 | from dataclasses import dataclass 6 | from datetime import datetime 7 | from enum import Enum, auto 8 | from typing import Callable, Protocol 9 | 10 | from quantdesk.utils.logging import get_logger 11 | 12 | log = get_logger(__name__) 13 | 14 | 15 | class EventType(Enum): 16 | MARKET = auto() 17 | SIGNAL = auto() 18 | ORDER = auto() 19 | FILL = auto() 20 | 21 | 22 | @dataclass(slots=True) 23 | class Event: 24 | type: EventType 25 | timestamp: datetime 26 | 27 | 28 | @dataclass(slots=True) 29 | class MarketEvent(Event): 30 | symbol: str 31 | price: float 32 | volume: float 33 | 34 | 35 | @dataclass(slots=True) 36 | class SignalEvent(Event): 37 | symbol: str 38 | direction: int # +1 long, -1 short 39 | strength: float 40 | 41 | 42 | @dataclass(slots=True) 43 | class OrderEvent(Event): 44 | symbol: str 45 | direction: int 46 | quantity: int 47 | order_type: str = "MKT" 48 | 49 | 50 | @dataclass(slots=True) 51 | class FillEvent(Event): 52 | symbol: str 53 | direction: int 54 | quantity: int 55 | fill_price: float 56 | commission: float 57 | 58 | 59 | class EventHandler(Protocol): 60 | def __call__(self, event: Event) -> None: ... 61 | 62 | 63 | class EventEngine: 64 | """Thread‑safe queue with pub/sub callbacks.""" 65 | 66 | def __init__(self) -> None: 67 | self._q: queue.Queue[Event] = queue.Queue(maxsize=10000) 68 | self._subs: dict[EventType, list[EventHandler]] = {t: [] for t in EventType} 69 | 70 | def put(self, event: Event) -> None: 71 | self._q.put(event, block=False) 72 | 73 | def subscribe(self, event_type: EventType, handler: EventHandler) -> None: 74 | self._subs[event_type].append(handler) 75 | 76 | def run_once(self) -> None: 77 | """Process a single event if available.""" 78 | try: 79 | event = self._q.get(block=False) 80 | except queue.Empty: 81 | return 82 | for handler in self._subs[event.type]: 83 | handler(event) 84 | self._q.task_done() -------------------------------------------------------------------------------- /quantdesk/core/event_engine.py.bak: -------------------------------------------------------------------------------- 1 | """Simple event queue driving Backtrader + live trading.""" 2 | from __future__ import annotations 3 | 4 | import queue 5 | from dataclasses import dataclass 6 | from datetime import datetime 7 | from enum import Enum, auto 8 | from typing import Callable, Protocol 9 | 10 | from quantdesk.utils.logging import get_logger 11 | 12 | log = get_logger(__name__) 13 | 14 | 15 | class EventType(Enum): 16 | MARKET = auto() 17 | SIGNAL = auto() 18 | ORDER = auto() 19 | FILL = auto() 20 | 21 | 22 | @dataclass(slots=True) 23 | class Event: 24 | type: EventType 25 | timestamp: datetime 26 | 27 | 28 | @dataclass(slots=True) 29 | class MarketEvent(Event): 30 | symbol: str 31 | price: float 32 | volume: float 33 | 34 | 35 | @dataclass(slots=True) 36 | class SignalEvent(Event): 37 | symbol: str 38 | direction: int # +1 long, -1 short 39 | strength: float 40 | 41 | 42 | @dataclass(slots=True) 43 | class OrderEvent(Event): 44 | symbol: str 45 | direction: int 46 | quantity: int 47 | order_type: str = "MKT" 48 | 49 | 50 | @dataclass(slots=True) 51 | class FillEvent(Event): 52 | symbol: str 53 | direction: int 54 | quantity: int 55 | fill_price: float 56 | commission: float 57 | 58 | 59 | class EventHandler(Protocol): 60 | def __call__(self, event: Event) -> None: ... 61 | 62 | 63 | class EventEngine: 64 | """Thread‑safe queue with pub/sub callbacks.""" 65 | 66 | def __init__(self) -> None: 67 | self._q: queue.Queue[Event] = queue.Queue(maxsize=10000) 68 | self._subs: dict[EventType, list[EventHandler]] = {t: [] for t in EventType} 69 | 70 | def put(self, event: Event) -> None: 71 | self._q.put(event, block=False) 72 | 73 | def subscribe(self, event_type: EventType, handler: EventHandler) -> None: 74 | self._subs[event_type].append(handler) 75 | 76 | def run_once(self) -> None: 77 | """Process a single event if available.""" 78 | try: 79 | event = self._q.get(block=False) 80 | except queue.Empty: 81 | return 82 | for handler in self._subs[event.type]: 83 | handler(event) 84 | self._q.task_done() -------------------------------------------------------------------------------- /algos/mean_reversion_sp100/config.yaml: -------------------------------------------------------------------------------- 1 | name: "Mean Reversion S&P 100" 2 | description: "Z-score based mean reversion strategy for S&P 100 constituents" 3 | version: "1.0.0" 4 | author: "QuantDesk Team" 5 | 6 | # Strategy parameters 7 | strategy: 8 | class: "algos.mean_reversion_sp100.strategy.MeanReversionSP100" 9 | params: 10 | lookback_period: 60 11 | return_period: 5 12 | zscore_threshold: 2.0 13 | max_positions: 3 14 | volatility_target: 0.10 15 | kelly_fraction: 0.25 16 | stop_loss: 0.03 17 | take_profit: 0.015 18 | printlog: false 19 | 20 | # Universe definition 21 | universe: 22 | provider: "alpaca" 23 | symbols: 24 | - "AAPL" 25 | - "MSFT" 26 | - "GOOGL" 27 | - "AMZN" 28 | - "TSLA" 29 | - "META" 30 | - "NVDA" 31 | - "JPM" 32 | - "JNJ" 33 | - "V" 34 | - "PG" 35 | - "UNH" 36 | - "HD" 37 | - "MA" 38 | - "DIS" 39 | - "PYPL" 40 | - "BAC" 41 | - "NFLX" 42 | - "ADBE" 43 | - "CRM" 44 | frequency: "1m" # 1-minute bars 45 | 46 | # Backtest configuration 47 | backtest: 48 | start_date: "2020-01-01" 49 | end_date: "2024-12-31" 50 | initial_cash: 100000 51 | commission: 0.005 # $0.005 per share 52 | slippage: 0.0005 # 5 basis points 53 | 54 | # Risk management 55 | risk: 56 | max_position_size: 10000 # USD 57 | max_daily_loss: 1000 # USD 58 | max_drawdown: 0.15 # 15% 59 | position_timeout: 1440 # Minutes (24 hours) 60 | 61 | # Performance targets (for validation) 62 | targets: 63 | sharpe_ratio: 1.2 64 | win_rate: 0.54 65 | max_drawdown: 0.04 66 | cagr: 0.18 67 | 68 | # Data requirements 69 | data: 70 | warmup_period: 120 # Minutes of data needed before trading 71 | required_fields: 72 | - "open" 73 | - "high" 74 | - "low" 75 | - "close" 76 | - "volume" 77 | 78 | # Execution settings 79 | execution: 80 | order_type: "market" 81 | time_in_force: "day" 82 | allow_fractional: false 83 | min_order_size: 1 84 | 85 | # Monitoring 86 | monitoring: 87 | log_level: "INFO" 88 | metrics_frequency: "1h" 89 | alert_thresholds: 90 | drawdown: 0.05 91 | position_count: 5 92 | daily_loss: 500 93 | 94 | # Paper trading 95 | paper_trading: 96 | enabled: true 97 | broker: "alpaca" 98 | initial_balance: 100000 99 | max_order_value: 5000 -------------------------------------------------------------------------------- /algos/mean_reversion_sp100/config.yaml.bak: -------------------------------------------------------------------------------- 1 | name: "Mean Reversion S&P 100" 2 | description: "Z-score based mean reversion strategy for S&P 100 constituents" 3 | version: "1.0.0" 4 | author: "QuantDesk Team" 5 | 6 | # Strategy parameters 7 | strategy: 8 | class: "algos.mean_reversion_sp100.strategy.MeanReversionSP100" 9 | params: 10 | lookback_period: 60 11 | return_period: 5 12 | zscore_threshold: 2.0 13 | max_positions: 3 14 | volatility_target: 0.10 15 | kelly_fraction: 0.25 16 | stop_loss: 0.03 17 | take_profit: 0.015 18 | printlog: false 19 | 20 | # Universe definition 21 | universe: 22 | provider: "alpaca" 23 | symbols: 24 | - "AAPL" 25 | - "MSFT" 26 | - "GOOGL" 27 | - "AMZN" 28 | - "TSLA" 29 | - "META" 30 | - "NVDA" 31 | - "JPM" 32 | - "JNJ" 33 | - "V" 34 | - "PG" 35 | - "UNH" 36 | - "HD" 37 | - "MA" 38 | - "DIS" 39 | - "PYPL" 40 | - "BAC" 41 | - "NFLX" 42 | - "ADBE" 43 | - "CRM" 44 | frequency: "1m" # 1-minute bars 45 | 46 | # Backtest configuration 47 | backtest: 48 | start_date: "2020-01-01" 49 | end_date: "2024-12-31" 50 | initial_cash: 100000 51 | commission: 0.005 # $0.005 per share 52 | slippage: 0.0005 # 5 basis points 53 | 54 | # Risk management 55 | risk: 56 | max_position_size: 10000 # USD 57 | max_daily_loss: 1000 # USD 58 | max_drawdown: 0.15 # 15% 59 | position_timeout: 1440 # Minutes (24 hours) 60 | 61 | # Performance targets (for validation) 62 | targets: 63 | sharpe_ratio: 1.2 64 | win_rate: 0.54 65 | max_drawdown: 0.04 66 | cagr: 0.18 67 | 68 | # Data requirements 69 | data: 70 | warmup_period: 120 # Minutes of data needed before trading 71 | required_fields: 72 | - "open" 73 | - "high" 74 | - "low" 75 | - "close" 76 | - "volume" 77 | 78 | # Execution settings 79 | execution: 80 | order_type: "market" 81 | time_in_force: "day" 82 | allow_fractional: false 83 | min_order_size: 1 84 | 85 | # Monitoring 86 | monitoring: 87 | log_level: "INFO" 88 | metrics_frequency: "1h" 89 | alert_thresholds: 90 | drawdown: 0.05 91 | position_count: 5 92 | daily_loss: 500 93 | 94 | # Paper trading 95 | paper_trading: 96 | enabled: true 97 | broker: "alpaca" 98 | initial_balance: 100000 99 | max_order_value: 5000 -------------------------------------------------------------------------------- /quantdesk/api/broker_router.py: -------------------------------------------------------------------------------- 1 | """Route orders to Alpaca or Binance.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from typing import Protocol 6 | 7 | import alpaca_trade_api as alpaca 8 | import ccxt.async_support as accxt 9 | from fastapi import HTTPException 10 | 11 | from quantdesk.utils.env import SETTINGS 12 | from quantdesk.utils.logging import get_logger 13 | from quantdesk.api.schemas import Order 14 | 15 | log = get_logger(__name__) 16 | 17 | 18 | class Broker(Protocol): 19 | async def submit(self, order: Order) -> str: ... 20 | async def cancel(self, order_id: str) -> None: ... 21 | 22 | 23 | class AlpacaBroker: 24 | def __init__(self) -> None: 25 | self._client = alpaca.REST( 26 | key_id=SETTINGS.alpaca_key, 27 | secret_key=SETTINGS.alpaca_secret, 28 | base_url="https://paper-api.alpaca.markets", 29 | ) 30 | 31 | async def submit(self, order: Order) -> str: 32 | o = await asyncio.to_thread( 33 | self._client.submit_order, 34 | symbol=order.symbol, 35 | qty=order.qty, 36 | side=order.side, 37 | type=order.order_type, 38 | time_in_force="day", 39 | limit_price=order.limit_price, 40 | ) 41 | return o.id 42 | 43 | async def cancel(self, order_id: str) -> None: 44 | await asyncio.to_thread(self._client.cancel_order, order_id) 45 | 46 | 47 | class BinanceBroker: 48 | def __init__(self) -> None: 49 | self._client = accxt.binance({ 50 | "apiKey": SETTINGS.binance_key, 51 | "secret": SETTINGS.binance_secret, 52 | "enableRateLimit": True, 53 | }) 54 | 55 | async def submit(self, order: Order) -> str: 56 | side = "buy" if order.side == "buy" else "sell" 57 | response = await self._client.create_order( 58 | symbol=order.symbol, 59 | type="MARKET" if order.order_type == "market" else "LIMIT", 60 | side=side.upper(), 61 | amount=order.qty, 62 | price=order.limit_price, 63 | ) 64 | return str(response["id"]) 65 | 66 | async def cancel(self, order_id: str) -> None: 67 | await self._client.cancel_order(order_id) 68 | 69 | 70 | class BrokerRouter: 71 | def __init__(self) -> None: 72 | self.alpaca = AlpacaBroker() 73 | self.binance = BinanceBroker() 74 | 75 | async def route(self, order: Order) -> str: 76 | if order.symbol.endswith("USD") or order.symbol.endswith("USDT"): 77 | return await self.binance.submit(order) 78 | if len(order.symbol) <= 5: # crude equity heuristic 79 | return await self.alpaca.submit(order) 80 | raise HTTPException(400, f"Un‑routable symbol {order.symbol}") -------------------------------------------------------------------------------- /quantdesk/api/broker_router.py.bak: -------------------------------------------------------------------------------- 1 | """Route orders to Alpaca or Binance.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from typing import Protocol 6 | 7 | import alpaca_trade_api as alpaca 8 | import ccxt.async_support as accxt 9 | from fastapi import HTTPException 10 | 11 | from quantdesk.utils.env import SETTINGS 12 | from quantdesk.utils.logging import get_logger 13 | from quantdesk.api.schemas import Order 14 | 15 | log = get_logger(__name__) 16 | 17 | 18 | class Broker(Protocol): 19 | async def submit(self, order: Order) -> str: ... 20 | async def cancel(self, order_id: str) -> None: ... 21 | 22 | 23 | class AlpacaBroker: 24 | def __init__(self) -> None: 25 | self._client = alpaca.REST( 26 | key_id=SETTINGS.alpaca_key, 27 | secret_key=SETTINGS.alpaca_secret, 28 | base_url="https://paper-api.alpaca.markets", 29 | ) 30 | 31 | async def submit(self, order: Order) -> str: 32 | o = await asyncio.to_thread( 33 | self._client.submit_order, 34 | symbol=order.symbol, 35 | qty=order.qty, 36 | side=order.side, 37 | type=order.order_type, 38 | time_in_force="day", 39 | limit_price=order.limit_price, 40 | ) 41 | return o.id 42 | 43 | async def cancel(self, order_id: str) -> None: 44 | await asyncio.to_thread(self._client.cancel_order, order_id) 45 | 46 | 47 | class BinanceBroker: 48 | def __init__(self) -> None: 49 | self._client = accxt.binance({ 50 | "apiKey": SETTINGS.binance_key, 51 | "secret": SETTINGS.binance_secret, 52 | "enableRateLimit": True, 53 | }) 54 | 55 | async def submit(self, order: Order) -> str: 56 | side = "buy" if order.side == "buy" else "sell" 57 | response = await self._client.create_order( 58 | symbol=order.symbol, 59 | type="MARKET" if order.order_type == "market" else "LIMIT", 60 | side=side.upper(), 61 | amount=order.qty, 62 | price=order.limit_price, 63 | ) 64 | return str(response["id"]) 65 | 66 | async def cancel(self, order_id: str) -> None: 67 | await self._client.cancel_order(order_id) 68 | 69 | 70 | class BrokerRouter: 71 | def __init__(self) -> None: 72 | self.alpaca = AlpacaBroker() 73 | self.binance = BinanceBroker() 74 | 75 | async def route(self, order: Order) -> str: 76 | if order.symbol.endswith("USD") or order.symbol.endswith("USDT"): 77 | return await self.binance.submit(order) 78 | if len(order.symbol) <= 5: # crude equity heuristic 79 | return await self.alpaca.submit(order) 80 | raise HTTPException(400, f"Un‑routable symbol {order.symbol}") -------------------------------------------------------------------------------- /quantdesk/stratlib/base_strategy.py: -------------------------------------------------------------------------------- 1 | """Abstract Backtrader strategy wrapper.""" 2 | from __future__ import annotations 3 | 4 | from abc import ABC, abstractmethod 5 | from datetime import datetime 6 | from typing import Any, Dict 7 | 8 | import backtrader as bt 9 | from quantdesk.utils.logging import get_logger 10 | 11 | 12 | class BaseStrategy(bt.Strategy, ABC): 13 | params: dict[str, Any] = { 14 | "size": 1, # position size in contracts/shares 15 | "atr_period": 14, 16 | "printlog": False, 17 | } 18 | 19 | def __init__(self) -> None: # noqa: D401; Backtrader API 20 | """Initialise common state & indicators.""" 21 | self.order: bt.Order | None = None # last submitted order 22 | self.log = get_logger(self.__class__.__name__) 23 | 24 | # Common indicators (sub-classes free to ignore/use) 25 | self.atr = bt.indicators.ATR(self.datas[0], period=self.params["atr_period"]) 26 | self.sma_fast = bt.indicators.SMA(self.datas[0], period=20) 27 | self.sma_slow = bt.indicators.SMA(self.datas[0], period=50) 28 | 29 | # ------------------------------------------------------- 30 | @abstractmethod 31 | def next(self) -> None: # noqa: D401; Backtrader API 32 | """Implement trading logic here (called every bar).""" 33 | 34 | # ------------------------------------------------------- 35 | # helper / callback utilities used by most concrete strats 36 | def _print(self, txt: str) -> None: # pragma: no cover 37 | """Console print if *printlog* param is truthy (non-critical).""" 38 | if self.params.get("printlog", False): 39 | dt = self.datas[0].datetime.datetime(0) 40 | print(f"{dt.isoformat()} {txt}") 41 | 42 | # Alias retained for backward compatibility 43 | _log = _print 44 | 45 | # ------------------------------------------------------- 46 | # Backtrader notifications 47 | def notify_order(self, order: bt.Order) -> None: # pragma: no cover 48 | if order.status in (order.Submitted, order.Accepted): 49 | return # not yet executed 50 | 51 | dt = self.datas[0].datetime.datetime(0) 52 | if order.status == order.Completed: 53 | side = "BUY" if order.isbuy() else "SELL" 54 | self.log.info( 55 | "order.completed", 56 | dt=dt.isoformat(), 57 | symbol=order.data._name, 58 | side=side, 59 | price=order.executed.price, 60 | qty=order.executed.size, 61 | value=order.executed.value, 62 | commission=order.executed.comm, 63 | ) 64 | self.order = None 65 | elif order.status in (order.Canceled, order.Margin, order.Rejected): 66 | self.log.warning( 67 | "order.failed", status=order.Status[order.status], dt=dt.isoformat() 68 | ) 69 | self.order = None 70 | 71 | def notify_trade(self, trade: bt.Trade) -> None: # pragma: no cover 72 | if trade.isclosed: 73 | self.log.info( 74 | "trade.closed", 75 | symbol=trade.data._name, 76 | pnl=trade.pnl, 77 | pnl_comm=trade.pnlcomm, 78 | barlen=trade.barlen, 79 | ) -------------------------------------------------------------------------------- /quantdesk/stratlib/base_strategy.py.bak: -------------------------------------------------------------------------------- 1 | """Abstract Backtrader strategy wrapper.""" 2 | from __future__ import annotations 3 | 4 | from abc import ABC, abstractmethod 5 | from datetime import datetime 6 | from typing import Any, Dict 7 | 8 | import backtrader as bt 9 | from quantdesk.utils.logging import get_logger 10 | 11 | 12 | class BaseStrategy(bt.Strategy, ABC): 13 | params: dict[str, Any] = { 14 | "size": 1, # position size in contracts/shares 15 | "atr_period": 14, 16 | "printlog": False, 17 | } 18 | 19 | def __init__(self) -> None: # noqa: D401; Backtrader API 20 | """Initialise common state & indicators.""" 21 | self.order: bt.Order | None = None # last submitted order 22 | self.log = get_logger(self.__class__.__name__) 23 | 24 | # Common indicators (sub-classes free to ignore/use) 25 | self.atr = bt.indicators.ATR(self.datas[0], period=self.params["atr_period"]) 26 | self.sma_fast = bt.indicators.SMA(self.datas[0], period=20) 27 | self.sma_slow = bt.indicators.SMA(self.datas[0], period=50) 28 | 29 | # ------------------------------------------------------- 30 | @abstractmethod 31 | def next(self) -> None: # noqa: D401; Backtrader API 32 | """Implement trading logic here (called every bar).""" 33 | 34 | # ------------------------------------------------------- 35 | # helper / callback utilities used by most concrete strats 36 | def _print(self, txt: str) -> None: # pragma: no cover 37 | """Console print if *printlog* param is truthy (non-critical).""" 38 | if self.params.get("printlog", False): 39 | dt = self.datas[0].datetime.datetime(0) 40 | print(f"{dt.isoformat()} {txt}") 41 | 42 | # Alias retained for backward compatibility 43 | _log = _print 44 | 45 | # ------------------------------------------------------- 46 | # Backtrader notifications 47 | def notify_order(self, order: bt.Order) -> None: # pragma: no cover 48 | if order.status in (order.Submitted, order.Accepted): 49 | return # not yet executed 50 | 51 | dt = self.datas[0].datetime.datetime(0) 52 | if order.status == order.Completed: 53 | side = "BUY" if order.isbuy() else "SELL" 54 | self.log.info( 55 | "order.completed", 56 | dt=dt.isoformat(), 57 | symbol=order.data._name, 58 | side=side, 59 | price=order.executed.price, 60 | qty=order.executed.size, 61 | value=order.executed.value, 62 | commission=order.executed.comm, 63 | ) 64 | self.order = None 65 | elif order.status in (order.Canceled, order.Margin, order.Rejected): 66 | self.log.warning( 67 | "order.failed", status=order.Status[order.status], dt=dt.isoformat() 68 | ) 69 | self.order = None 70 | 71 | def notify_trade(self, trade: bt.Trade) -> None: # pragma: no cover 72 | if trade.isclosed: 73 | self.log.info( 74 | "trade.closed", 75 | symbol=trade.data._name, 76 | pnl=trade.pnl, 77 | pnl_comm=trade.pnlcomm, 78 | barlen=trade.barlen, 79 | ) -------------------------------------------------------------------------------- /quantdesk/core/data_loader.py: -------------------------------------------------------------------------------- 1 | """Unified data access across Yahoo, Alpaca, Polygon, Binance‑ccxt.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from datetime import datetime 6 | from typing import Any, Literal 7 | 8 | import pandas as pd 9 | import yfinance as yf 10 | from alpaca.data.historical import StockHistoricalDataClient 11 | from alpaca.data.requests import StockBarsRequest 12 | from alpaca.data.timeframe import TimeFrame 13 | import ccxt 14 | 15 | from quantdesk.utils.logging import get_logger 16 | from quantdesk.utils.env import SETTINGS 17 | 18 | log = get_logger(__name__) 19 | 20 | Provider = Literal["yahoo", "alpaca", "binance"] 21 | Freq = Literal["1m", "5m", "1h", "1d"] 22 | 23 | _alpaca_client = StockHistoricalDataClient( 24 | api_key=SETTINGS.alpaca_key, secret_key=SETTINGS.alpaca_secret 25 | ) 26 | 27 | _binance = ccxt.binance() 28 | if SETTINGS.binance_key and SETTINGS.binance_secret: 29 | _binance = ccxt.binance({ 30 | "apiKey": SETTINGS.binance_key, 31 | "secret": SETTINGS.binance_secret, 32 | "enableRateLimit": True, 33 | }) 34 | 35 | 36 | def _load_yahoo(symbol: str, start: datetime, end: datetime, freq: Freq) -> pd.DataFrame: 37 | interval_map: dict[Freq, str] = {"1m": "1m", "5m": "5m", "1h": "60m", "1d": "1d"} 38 | df = yf.download( 39 | tickers=symbol, 40 | start=start, 41 | end=end, 42 | interval=interval_map[freq], 43 | progress=False, 44 | auto_adjust=False, 45 | threads=True, 46 | ) 47 | return df.rename(columns=str.lower) # std: open, high, low, close, adj close, volume 48 | 49 | 50 | def _load_alpaca(symbol: str, start: datetime, end: datetime, freq: Freq) -> pd.DataFrame: 51 | tf_map: dict[Freq, TimeFrame] = { 52 | "1m": TimeFrame.Minute, 53 | "5m": TimeFrame(5, TimeFrame.Unit.Minute), 54 | "1h": TimeFrame.Hour, 55 | "1d": TimeFrame.Day, 56 | } 57 | req = StockBarsRequest(symbol_or_symbols=symbol, timeframe=tf_map[freq], start=start, end=end) 58 | bars = _alpaca_client.get_stock_bars(req).df 59 | return bars.xs(symbol, level=0).rename(columns=str.lower) 60 | 61 | 62 | async def _load_binance(symbol: str, start: datetime, end: datetime, freq: Freq) -> pd.DataFrame: 63 | limit = 1000 64 | timeframe = freq 65 | since = int(start.timestamp() * 1000) 66 | out: list[list[Any]] = [] 67 | 68 | while since < end.timestamp() * 1000: 69 | batch = await asyncio.to_thread( 70 | _binance.fetch_ohlcv, symbol, timeframe=timeframe, since=since, limit=limit 71 | ) 72 | if not batch: 73 | break 74 | since = batch[-1][0] + 1 75 | out.extend(batch) 76 | 77 | df = pd.DataFrame( 78 | out, columns=["timestamp", "open", "high", "low", "close", "volume"] 79 | ) 80 | df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True) 81 | df.set_index("timestamp", inplace=True) 82 | return df 83 | 84 | 85 | def load( 86 | symbol: str, 87 | start: datetime, 88 | end: datetime, 89 | freq: Freq = "1d", 90 | provider: Provider = "yahoo", 91 | ) -> pd.DataFrame: 92 | """Public entry point.""" 93 | log.info("load.start", symbol=symbol, provider=provider, freq=freq) 94 | if provider == "yahoo": 95 | return _load_yahoo(symbol, start, end, freq) 96 | if provider == "alpaca": 97 | return _load_alpaca(symbol, start, end, freq) 98 | if provider == "binance": 99 | return asyncio.run(_load_binance(symbol, start, end, freq)) 100 | raise ValueError(f"Unsupported provider {provider}") -------------------------------------------------------------------------------- /quantdesk/core/data_loader.py.bak: -------------------------------------------------------------------------------- 1 | """Unified data access across Yahoo, Alpaca, Polygon, Binance‑ccxt.""" 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from datetime import datetime 6 | from typing import Any, Literal 7 | 8 | import pandas as pd 9 | import yfinance as yf 10 | from alpaca.data.historical import StockHistoricalDataClient 11 | from alpaca.data.requests import StockBarsRequest 12 | from alpaca.data.timeframe import TimeFrame 13 | import ccxt 14 | 15 | from quantdesk.utils.logging import get_logger 16 | from quantdesk.utils.env import SETTINGS 17 | 18 | log = get_logger(__name__) 19 | 20 | Provider = Literal["yahoo", "alpaca", "binance"] 21 | Freq = Literal["1m", "5m", "1h", "1d"] 22 | 23 | _alpaca_client = StockHistoricalDataClient( 24 | api_key=SETTINGS.alpaca_key, secret_key=SETTINGS.alpaca_secret 25 | ) 26 | 27 | _binance = ccxt.binance() 28 | if SETTINGS.binance_key and SETTINGS.binance_secret: 29 | _binance = ccxt.binance({ 30 | "apiKey": SETTINGS.binance_key, 31 | "secret": SETTINGS.binance_secret, 32 | "enableRateLimit": True, 33 | }) 34 | 35 | 36 | def _load_yahoo(symbol: str, start: datetime, end: datetime, freq: Freq) -> pd.DataFrame: 37 | interval_map: dict[Freq, str] = {"1m": "1m", "5m": "5m", "1h": "60m", "1d": "1d"} 38 | df = yf.download( 39 | tickers=symbol, 40 | start=start, 41 | end=end, 42 | interval=interval_map[freq], 43 | progress=False, 44 | auto_adjust=False, 45 | threads=True, 46 | ) 47 | return df.rename(columns=str.lower) # std: open, high, low, close, adj close, volume 48 | 49 | 50 | def _load_alpaca(symbol: str, start: datetime, end: datetime, freq: Freq) -> pd.DataFrame: 51 | tf_map: dict[Freq, TimeFrame] = { 52 | "1m": TimeFrame.Minute, 53 | "5m": TimeFrame(5, TimeFrame.Unit.Minute), 54 | "1h": TimeFrame.Hour, 55 | "1d": TimeFrame.Day, 56 | } 57 | req = StockBarsRequest(symbol_or_symbols=symbol, timeframe=tf_map[freq], start=start, end=end) 58 | bars = _alpaca_client.get_stock_bars(req).df 59 | return bars.xs(symbol, level=0).rename(columns=str.lower) 60 | 61 | 62 | async def _load_binance(symbol: str, start: datetime, end: datetime, freq: Freq) -> pd.DataFrame: 63 | limit = 1000 64 | timeframe = freq 65 | since = int(start.timestamp() * 1000) 66 | out: list[list[Any]] = [] 67 | 68 | while since < end.timestamp() * 1000: 69 | batch = await asyncio.to_thread( 70 | _binance.fetch_ohlcv, symbol, timeframe=timeframe, since=since, limit=limit 71 | ) 72 | if not batch: 73 | break 74 | since = batch[-1][0] + 1 75 | out.extend(batch) 76 | 77 | df = pd.DataFrame( 78 | out, columns=["timestamp", "open", "high", "low", "close", "volume"] 79 | ) 80 | df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True) 81 | df.set_index("timestamp", inplace=True) 82 | return df 83 | 84 | 85 | def load( 86 | symbol: str, 87 | start: datetime, 88 | end: datetime, 89 | freq: Freq = "1d", 90 | provider: Provider = "yahoo", 91 | ) -> pd.DataFrame: 92 | """Public entry point.""" 93 | log.info("load.start", symbol=symbol, provider=provider, freq=freq) 94 | if provider == "yahoo": 95 | return _load_yahoo(symbol, start, end, freq) 96 | if provider == "alpaca": 97 | return _load_alpaca(symbol, start, end, freq) 98 | if provider == "binance": 99 | return asyncio.run(_load_binance(symbol, start, end, freq)) 100 | raise ValueError(f"Unsupported provider {provider}") -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "quantdesk" 3 | version = "0.1.0" 4 | description = "Open-source quantitative trading research platform" 5 | authors = ["QuantDesk Team "] 6 | readme = "README.md" 7 | license = "AGPL-3.0" 8 | homepage = "https://github.com/quantdesk/quantdesk" 9 | repository = "https://github.com/quantdesk/quantdesk" 10 | documentation = "https://quantdesk.readthedocs.io" 11 | keywords = ["quantitative", "trading", "backtesting", "finance", "research"] 12 | classifiers = [ 13 | "Development Status :: 3 - Alpha", 14 | "Intended Audience :: Financial and Insurance Industry", 15 | "License :: OSI Approved :: GNU Affero General Public License v3", 16 | "Programming Language :: Python :: 3.11", 17 | "Topic :: Office/Business :: Financial :: Investment", 18 | ] 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.11" 22 | numpy = "^1.26.0" 23 | pandas = "^2.2.0" 24 | scipy = "^1.13.0" 25 | statsmodels = "^0.14.0" 26 | ta = "^0.10.2" 27 | backtrader = "^1.9.76" 28 | vectorbt = "^0.25.0" 29 | mlflow = "^2.14.0" 30 | scikit-learn = "^1.5.0" 31 | torch = "^2.3.0" 32 | yfinance = "^0.2.40" 33 | alpaca-trade-api = "^3.2.0" 34 | ccxt = "^4.3.0" 35 | fastapi = "^0.111.0" 36 | uvicorn = {extras = ["standard"], version = "^0.30.0"} 37 | pydantic = "^2.8.0" 38 | python-dotenv = "^1.0.1" 39 | structlog = "^24.1.0" 40 | scikit-optimize = "^0.9.0" 41 | arch = "^5.6.0" 42 | quantstats = "^0.0.62" 43 | click = "^8.1.0" 44 | websockets = "^10.0" 45 | aiohttp = "^3.8.0" 46 | redis = "^5.0.0" 47 | psycopg2-binary = "^2.9.0" 48 | sqlalchemy = "^2.0.0" 49 | alembic = "^1.13.0" 50 | apscheduler = "^3.10.0" 51 | prometheus-client = "^0.20.0" 52 | discord-webhook = "^1.3.0" 53 | plotly = "^5.22.0" 54 | pyarrow = "^15.0.0" 55 | fastavro = "^1.9.0" 56 | jupyterlab = "^4.2.0" 57 | ipywidgets = "^8.1.0" 58 | seaborn = "^0.13.0" 59 | matplotlib = "^3.9.0" 60 | 61 | [tool.poetry.group.dev.dependencies] 62 | ruff = "^0.4.0" 63 | black = "^24.3.0" 64 | mypy = "^1.10.0" 65 | pytest = "^8.2.0" 66 | pytest-cov = "^5.0.0" 67 | pytest-xdist = "^3.6.0" 68 | pytest-asyncio = "^0.23.0" 69 | bandit = "^1.7.0" 70 | pre-commit = "^3.7.0" 71 | mkdocs-material = "^9.5.0" 72 | mkdocstrings = {extras = ["python"], version = "^0.25.0"} 73 | jupyter-book = "^1.0.0" 74 | pdoc = "^14.5.0" 75 | 76 | [tool.poetry.group.test.dependencies] 77 | httpx = "^0.27.0" 78 | pytest-mock = "^3.14.0" 79 | factory-boy = "^3.3.0" 80 | freezegun = "^1.5.0" 81 | 82 | [tool.poetry.scripts] 83 | quantdesk = "quantdesk.cli:main" 84 | fetch-yf = "scripts.fetch_yf:main" 85 | 86 | [build-system] 87 | requires = ["poetry-core"] 88 | build-backend = "poetry.core.masonry.api" 89 | 90 | [tool.black] 91 | line-length = 100 92 | target-version = ["py311"] 93 | include = '\.pyi?$' 94 | extend-exclude = ''' 95 | /( 96 | # directories 97 | \.eggs 98 | | \.git 99 | | \.hg 100 | | \.mypy_cache 101 | | \.tox 102 | | \.venv 103 | | build 104 | | dist 105 | )/ 106 | ''' 107 | 108 | [tool.ruff] 109 | target-version = "py311" 110 | line-length = 100 111 | select = ["ALL"] 112 | ignore = [ 113 | "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", # Missing docstrings 114 | "ANN101", "ANN102", # Missing type annotation for self/cls 115 | "COM812", "ISC001", # Conflicts with formatter 116 | "E501", # Line too long (handled by black) 117 | "PLR0913", # Too many arguments 118 | "PLR2004", # Magic value used in comparison 119 | ] 120 | 121 | [tool.ruff.per-file-ignores] 122 | "tests/*" = ["S101", "PLR2004", "SLF001"] 123 | "scripts/*" = ["T201"] 124 | 125 | [tool.mypy] 126 | python_version = "3.11" 127 | strict = true 128 | warn_return_any = true 129 | warn_unused_configs = true 130 | disallow_untyped_defs = true 131 | disallow_incomplete_defs = true 132 | check_untyped_defs = true 133 | disallow_untyped_decorators = true 134 | no_implicit_optional = true 135 | warn_redundant_casts = true 136 | warn_unused_ignores = true 137 | warn_no_return = true 138 | warn_unreachable = true 139 | strict_equality = true 140 | 141 | [[tool.mypy.overrides]] 142 | module = [ 143 | "backtrader.*", 144 | "vectorbt.*", 145 | "ta.*", 146 | "ccxt.*", 147 | "alpaca_trade_api.*", 148 | "quantstats.*", 149 | ] 150 | ignore_missing_imports = true 151 | 152 | [tool.pytest.ini_options] 153 | minversion = "6.0" 154 | addopts = "-ra -q --strict-markers --strict-config" 155 | testpaths = ["tests"] 156 | python_files = ["test_*.py", "*_test.py"] 157 | python_classes = ["Test*"] 158 | python_functions = ["test_*"] 159 | markers = [ 160 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 161 | "integration: marks tests as integration tests", 162 | "unit: marks tests as unit tests", 163 | ] 164 | 165 | [tool.coverage.run] 166 | source = ["quantdesk"] 167 | omit = ["*/tests/*", "*/test_*"] 168 | 169 | [tool.coverage.report] 170 | exclude_lines = [ 171 | "pragma: no cover", 172 | "def __repr__", 173 | "raise AssertionError", 174 | "raise NotImplementedError", 175 | ] -------------------------------------------------------------------------------- /pyproject.toml.bak: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "quantdesk" 3 | version = "0.1.0" 4 | description = "Open-source quantitative trading research platform" 5 | authors = ["QuantDesk Team "] 6 | readme = "README.md" 7 | license = "AGPL-3.0" 8 | homepage = "https://github.com/quantdesk/quantdesk" 9 | repository = "https://github.com/quantdesk/quantdesk" 10 | documentation = "https://quantdesk.readthedocs.io" 11 | keywords = ["quantitative", "trading", "backtesting", "finance", "research"] 12 | classifiers = [ 13 | "Development Status :: 3 - Alpha", 14 | "Intended Audience :: Financial and Insurance Industry", 15 | "License :: OSI Approved :: GNU Affero General Public License v3", 16 | "Programming Language :: Python :: 3.11", 17 | "Topic :: Office/Business :: Financial :: Investment", 18 | ] 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.11" 22 | numpy = "^1.26.0" 23 | pandas = "^2.2.0" 24 | scipy = "^1.13.0" 25 | statsmodels = "^0.14.0" 26 | ta = "^0.10.2" 27 | backtrader = "^1.9.76" 28 | vectorbt = "^0.25.0" 29 | mlflow = "^2.14.0" 30 | scikit-learn = "^1.5.0" 31 | torch = "^2.3.0" 32 | yfinance = "^0.2.40" 33 | alpaca-trade-api = "^3.2.0" 34 | ccxt = "^4.3.0" 35 | fastapi = "^0.111.0" 36 | uvicorn = {extras = ["standard"], version = "^0.30.0"} 37 | pydantic = "^2.8.0" 38 | python-dotenv = "^1.0.1" 39 | structlog = "^24.1.0" 40 | scikit-optimize = "^0.9.0" 41 | arch = "^5.6.0" 42 | quantstats = "^0.0.62" 43 | click = "^8.1.0" 44 | websockets = "^10.0" 45 | aiohttp = "^3.8.0" 46 | redis = "^5.0.0" 47 | psycopg2-binary = "^2.9.0" 48 | sqlalchemy = "^2.0.0" 49 | alembic = "^1.13.0" 50 | apscheduler = "^3.10.0" 51 | prometheus-client = "^0.20.0" 52 | discord-webhook = "^1.3.0" 53 | plotly = "^5.22.0" 54 | pyarrow = "^15.0.0" 55 | fastavro = "^1.9.0" 56 | jupyterlab = "^4.2.0" 57 | ipywidgets = "^8.1.0" 58 | seaborn = "^0.13.0" 59 | matplotlib = "^3.9.0" 60 | 61 | [tool.poetry.group.dev.dependencies] 62 | ruff = "^0.4.0" 63 | black = "^24.3.0" 64 | mypy = "^1.10.0" 65 | pytest = "^8.2.0" 66 | pytest-cov = "^5.0.0" 67 | pytest-xdist = "^3.6.0" 68 | pytest-asyncio = "^0.23.0" 69 | bandit = "^1.7.0" 70 | pre-commit = "^3.7.0" 71 | mkdocs-material = "^9.5.0" 72 | mkdocstrings = {extras = ["python"], version = "^0.25.0"} 73 | jupyter-book = "^1.0.0" 74 | pdoc = "^14.5.0" 75 | 76 | [tool.poetry.group.test.dependencies] 77 | httpx = "^0.27.0" 78 | pytest-mock = "^3.14.0" 79 | factory-boy = "^3.3.0" 80 | freezegun = "^1.5.0" 81 | 82 | [tool.poetry.scripts] 83 | quantdesk = "quantdesk.cli:main" 84 | fetch-yf = "scripts.fetch_yf:main" 85 | 86 | [build-system] 87 | requires = ["poetry-core"] 88 | build-backend = "poetry.core.masonry.api" 89 | 90 | [tool.black] 91 | line-length = 100 92 | target-version = ["py311"] 93 | include = '\.pyi?$' 94 | extend-exclude = ''' 95 | /( 96 | # directories 97 | \.eggs 98 | | \.git 99 | | \.hg 100 | | \.mypy_cache 101 | | \.tox 102 | | \.venv 103 | | build 104 | | dist 105 | )/ 106 | ''' 107 | 108 | [tool.ruff] 109 | target-version = "py311" 110 | line-length = 100 111 | select = ["ALL"] 112 | ignore = [ 113 | "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", # Missing docstrings 114 | "ANN101", "ANN102", # Missing type annotation for self/cls 115 | "COM812", "ISC001", # Conflicts with formatter 116 | "E501", # Line too long (handled by black) 117 | "PLR0913", # Too many arguments 118 | "PLR2004", # Magic value used in comparison 119 | ] 120 | 121 | [tool.ruff.per-file-ignores] 122 | "tests/*" = ["S101", "PLR2004", "SLF001"] 123 | "scripts/*" = ["T201"] 124 | 125 | [tool.mypy] 126 | python_version = "3.11" 127 | strict = true 128 | warn_return_any = true 129 | warn_unused_configs = true 130 | disallow_untyped_defs = true 131 | disallow_incomplete_defs = true 132 | check_untyped_defs = true 133 | disallow_untyped_decorators = true 134 | no_implicit_optional = true 135 | warn_redundant_casts = true 136 | warn_unused_ignores = true 137 | warn_no_return = true 138 | warn_unreachable = true 139 | strict_equality = true 140 | 141 | [[tool.mypy.overrides]] 142 | module = [ 143 | "backtrader.*", 144 | "vectorbt.*", 145 | "ta.*", 146 | "ccxt.*", 147 | "alpaca_trade_api.*", 148 | "quantstats.*", 149 | ] 150 | ignore_missing_imports = true 151 | 152 | [tool.pytest.ini_options] 153 | minversion = "6.0" 154 | addopts = "-ra -q --strict-markers --strict-config" 155 | testpaths = ["tests"] 156 | python_files = ["test_*.py", "*_test.py"] 157 | python_classes = ["Test*"] 158 | python_functions = ["test_*"] 159 | markers = [ 160 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 161 | "integration: marks tests as integration tests", 162 | "unit: marks tests as unit tests", 163 | ] 164 | 165 | [tool.coverage.run] 166 | source = ["quantdesk"] 167 | omit = ["*/tests/*", "*/test_*"] 168 | 169 | [tool.coverage.report] 170 | exclude_lines = [ 171 | "pragma: no cover", 172 | "def __repr__", 173 | "raise AssertionError", 174 | "raise NotImplementedError", 175 | ] -------------------------------------------------------------------------------- /quantdesk/core/metrics.py: -------------------------------------------------------------------------------- 1 | """Performance metrics with deflated Sharpe ratio.""" 2 | from __future__ import annotations 3 | 4 | import math 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | import numpy as np 9 | import pandas as pd 10 | from scipy.stats import norm 11 | 12 | try: 13 | import quantstats as qs 14 | HAS_QUANTSTATS = True 15 | except ImportError: 16 | HAS_QUANTSTATS = False 17 | 18 | from quantdesk.utils.logging import get_logger 19 | 20 | log = get_logger(__name__) 21 | 22 | TRADING_DAYS = 252 23 | 24 | 25 | def sharpe_ratio(returns: pd.Series, rf: float = 0.0) -> float: 26 | excess = returns - rf / TRADING_DAYS 27 | return np.sqrt(TRADING_DAYS) * excess.mean() / excess.std(ddof=1) 28 | 29 | 30 | def deflated_sharpe( 31 | sharpes: np.ndarray, n_trials: int, n_obs: int, skew: float, kurt: float 32 | ) -> np.ndarray: 33 | """Implements Bailey & Lopez‑de‑Prado (2020).""" 34 | sr = sharpes 35 | sr_hat = sr * np.sqrt((n_obs - 1) / (n_obs - 2)) 36 | z = sr_hat * np.sqrt(n_obs - 1) 37 | delta = 0.5 * (skew * sr_hat + ((kurt - 3) / 4) * sr_hat**2) 38 | p = 1.0 - norm.cdf(z + delta) 39 | deflated = sr_hat - norm.ppf(1.0 - p / n_trials) / np.sqrt(n_obs - 1) 40 | return deflated 41 | 42 | 43 | def tearsheet(returns: pd.Series) -> dict[str, float]: 44 | sr = sharpe_ratio(returns) 45 | df = pd.DataFrame({"r": returns}) 46 | skew = df["r"].skew() 47 | kurt = df["r"].kurtosis() 48 | dsr = float(deflated_sharpe(np.array([sr]), 1, len(df), skew, kurt)) 49 | cagr = (1 + returns).prod() ** (TRADING_DAYS / len(returns)) - 1 50 | mdd = (df["r"].cumsum().expanding().max() - df["r"].cumsum()).max() 51 | return {"sharpe": sr, "deflated_sharpe": dsr, "cagr": cagr, "max_drawdown": mdd} 52 | 53 | 54 | def quantstats_report( 55 | returns: pd.Series, 56 | benchmark: pd.Series | None = None, 57 | output_file: str | Path | None = None, 58 | title: str = "Strategy Performance", 59 | ) -> dict[str, Any] | None: 60 | """Generate comprehensive QuantStats performance report. 61 | 62 | :param returns: Strategy returns series. 63 | :param benchmark: Benchmark returns series (optional). 64 | :param output_file: Path to save HTML report (optional). 65 | :param title: Report title. 66 | :return: Dictionary of metrics or None if QuantStats not available. 67 | """ 68 | if not HAS_QUANTSTATS: 69 | log.warning("QuantStats not installed - skipping report generation") 70 | return None 71 | 72 | # Configure QuantStats 73 | qs.extend_pandas() 74 | 75 | # Generate metrics 76 | metrics = { 77 | "total_return": qs.stats.comp(returns), 78 | "cagr": qs.stats.cagr(returns), 79 | "volatility": qs.stats.volatility(returns), 80 | "sharpe": qs.stats.sharpe(returns), 81 | "sortino": qs.stats.sortino(returns), 82 | "max_drawdown": qs.stats.max_drawdown(returns), 83 | "calmar": qs.stats.calmar(returns), 84 | "skew": qs.stats.skew(returns), 85 | "kurtosis": qs.stats.kurtosis(returns), 86 | "tail_ratio": qs.stats.tail_ratio(returns), 87 | "common_sense_ratio": qs.stats.common_sense_ratio(returns), 88 | "value_at_risk": qs.stats.value_at_risk(returns), 89 | "conditional_value_at_risk": qs.stats.conditional_value_at_risk(returns), 90 | } 91 | 92 | # Add benchmark-relative metrics if provided 93 | if benchmark is not None: 94 | metrics.update({ 95 | "alpha": qs.stats.alpha(returns, benchmark), 96 | "beta": qs.stats.beta(returns, benchmark), 97 | "information_ratio": qs.stats.information_ratio(returns, benchmark), 98 | "treynor_ratio": qs.stats.treynor_ratio(returns, benchmark), 99 | }) 100 | 101 | # Generate HTML report if output file specified 102 | if output_file: 103 | output_path = Path(output_file) 104 | output_path.parent.mkdir(parents=True, exist_ok=True) 105 | 106 | if benchmark is not None: 107 | qs.reports.html(returns, benchmark, output=str(output_path), title=title) 108 | else: 109 | qs.reports.html(returns, output=str(output_path), title=title) 110 | 111 | log.info("quantstats.report_generated", path=str(output_path)) 112 | 113 | return metrics 114 | 115 | 116 | def quantstats_tearsheet( 117 | returns: pd.Series, 118 | benchmark: pd.Series | None = None, 119 | live_start_date: str | None = None, 120 | ) -> None: 121 | """Display QuantStats tearsheet in console/notebook. 122 | 123 | :param returns: Strategy returns series. 124 | :param benchmark: Benchmark returns series (optional). 125 | :param live_start_date: Date when live trading started (optional). 126 | """ 127 | if not HAS_QUANTSTATS: 128 | log.warning("QuantStats not installed - skipping tearsheet") 129 | return 130 | 131 | qs.extend_pandas() 132 | 133 | if benchmark is not None: 134 | qs.reports.full(returns, benchmark, live_start_date=live_start_date) 135 | else: 136 | qs.reports.basic(returns, live_start_date=live_start_date) 137 | 138 | 139 | def combined_metrics( 140 | returns: pd.Series, 141 | benchmark: pd.Series | None = None, 142 | include_quantstats: bool = True, 143 | ) -> dict[str, Any]: 144 | """Combine custom metrics with QuantStats metrics. 145 | 146 | :param returns: Strategy returns series. 147 | :param benchmark: Benchmark returns series (optional). 148 | :param include_quantstats: Whether to include QuantStats metrics. 149 | :return: Combined metrics dictionary. 150 | """ 151 | # Start with existing custom metrics 152 | metrics = tearsheet(returns) 153 | 154 | # Add QuantStats metrics if available and requested 155 | if include_quantstats and HAS_QUANTSTATS: 156 | qs_metrics = quantstats_report(returns, benchmark) 157 | if qs_metrics: 158 | metrics.update({"quantstats": qs_metrics}) 159 | 160 | return metrics -------------------------------------------------------------------------------- /quantdesk/core/metrics.py.bak: -------------------------------------------------------------------------------- 1 | """Performance metrics with deflated Sharpe ratio.""" 2 | from __future__ import annotations 3 | 4 | import math 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | import numpy as np 9 | import pandas as pd 10 | from scipy.stats import norm 11 | 12 | try: 13 | import quantstats as qs 14 | HAS_QUANTSTATS = True 15 | except ImportError: 16 | HAS_QUANTSTATS = False 17 | 18 | from quantdesk.utils.logging import get_logger 19 | 20 | log = get_logger(__name__) 21 | 22 | TRADING_DAYS = 252 23 | 24 | 25 | def sharpe_ratio(returns: pd.Series, rf: float = 0.0) -> float: 26 | excess = returns - rf / TRADING_DAYS 27 | return np.sqrt(TRADING_DAYS) * excess.mean() / excess.std(ddof=1) 28 | 29 | 30 | def deflated_sharpe( 31 | sharpes: np.ndarray, n_trials: int, n_obs: int, skew: float, kurt: float 32 | ) -> np.ndarray: 33 | """Implements Bailey & Lopez‑de‑Prado (2020).""" 34 | sr = sharpes 35 | sr_hat = sr * np.sqrt((n_obs - 1) / (n_obs - 2)) 36 | z = sr_hat * np.sqrt(n_obs - 1) 37 | delta = 0.5 * (skew * sr_hat + ((kurt - 3) / 4) * sr_hat**2) 38 | p = 1.0 - norm.cdf(z + delta) 39 | deflated = sr_hat - norm.ppf(1.0 - p / n_trials) / np.sqrt(n_obs - 1) 40 | return deflated 41 | 42 | 43 | def tearsheet(returns: pd.Series) -> dict[str, float]: 44 | sr = sharpe_ratio(returns) 45 | df = pd.DataFrame({"r": returns}) 46 | skew = df["r"].skew() 47 | kurt = df["r"].kurtosis() 48 | dsr = float(deflated_sharpe(np.array([sr]), 1, len(df), skew, kurt)) 49 | cagr = (1 + returns).prod() ** (TRADING_DAYS / len(returns)) - 1 50 | mdd = (df["r"].cumsum().expanding().max() - df["r"].cumsum()).max() 51 | return {"sharpe": sr, "deflated_sharpe": dsr, "cagr": cagr, "max_drawdown": mdd} 52 | 53 | 54 | def quantstats_report( 55 | returns: pd.Series, 56 | benchmark: pd.Series | None = None, 57 | output_file: str | Path | None = None, 58 | title: str = "Strategy Performance", 59 | ) -> dict[str, Any] | None: 60 | """Generate comprehensive QuantStats performance report. 61 | 62 | :param returns: Strategy returns series. 63 | :param benchmark: Benchmark returns series (optional). 64 | :param output_file: Path to save HTML report (optional). 65 | :param title: Report title. 66 | :return: Dictionary of metrics or None if QuantStats not available. 67 | """ 68 | if not HAS_QUANTSTATS: 69 | log.warning("QuantStats not installed - skipping report generation") 70 | return None 71 | 72 | # Configure QuantStats 73 | qs.extend_pandas() 74 | 75 | # Generate metrics 76 | metrics = { 77 | "total_return": qs.stats.comp(returns), 78 | "cagr": qs.stats.cagr(returns), 79 | "volatility": qs.stats.volatility(returns), 80 | "sharpe": qs.stats.sharpe(returns), 81 | "sortino": qs.stats.sortino(returns), 82 | "max_drawdown": qs.stats.max_drawdown(returns), 83 | "calmar": qs.stats.calmar(returns), 84 | "skew": qs.stats.skew(returns), 85 | "kurtosis": qs.stats.kurtosis(returns), 86 | "tail_ratio": qs.stats.tail_ratio(returns), 87 | "common_sense_ratio": qs.stats.common_sense_ratio(returns), 88 | "value_at_risk": qs.stats.value_at_risk(returns), 89 | "conditional_value_at_risk": qs.stats.conditional_value_at_risk(returns), 90 | } 91 | 92 | # Add benchmark-relative metrics if provided 93 | if benchmark is not None: 94 | metrics.update({ 95 | "alpha": qs.stats.alpha(returns, benchmark), 96 | "beta": qs.stats.beta(returns, benchmark), 97 | "information_ratio": qs.stats.information_ratio(returns, benchmark), 98 | "treynor_ratio": qs.stats.treynor_ratio(returns, benchmark), 99 | }) 100 | 101 | # Generate HTML report if output file specified 102 | if output_file: 103 | output_path = Path(output_file) 104 | output_path.parent.mkdir(parents=True, exist_ok=True) 105 | 106 | if benchmark is not None: 107 | qs.reports.html(returns, benchmark, output=str(output_path), title=title) 108 | else: 109 | qs.reports.html(returns, output=str(output_path), title=title) 110 | 111 | log.info("quantstats.report_generated", path=str(output_path)) 112 | 113 | return metrics 114 | 115 | 116 | def quantstats_tearsheet( 117 | returns: pd.Series, 118 | benchmark: pd.Series | None = None, 119 | live_start_date: str | None = None, 120 | ) -> None: 121 | """Display QuantStats tearsheet in console/notebook. 122 | 123 | :param returns: Strategy returns series. 124 | :param benchmark: Benchmark returns series (optional). 125 | :param live_start_date: Date when live trading started (optional). 126 | """ 127 | if not HAS_QUANTSTATS: 128 | log.warning("QuantStats not installed - skipping tearsheet") 129 | return 130 | 131 | qs.extend_pandas() 132 | 133 | if benchmark is not None: 134 | qs.reports.full(returns, benchmark, live_start_date=live_start_date) 135 | else: 136 | qs.reports.basic(returns, live_start_date=live_start_date) 137 | 138 | 139 | def combined_metrics( 140 | returns: pd.Series, 141 | benchmark: pd.Series | None = None, 142 | include_quantstats: bool = True, 143 | ) -> dict[str, Any]: 144 | """Combine custom metrics with QuantStats metrics. 145 | 146 | :param returns: Strategy returns series. 147 | :param benchmark: Benchmark returns series (optional). 148 | :param include_quantstats: Whether to include QuantStats metrics. 149 | :return: Combined metrics dictionary. 150 | """ 151 | # Start with existing custom metrics 152 | metrics = tearsheet(returns) 153 | 154 | # Add QuantStats metrics if available and requested 155 | if include_quantstats and HAS_QUANTSTATS: 156 | qs_metrics = quantstats_report(returns, benchmark) 157 | if qs_metrics: 158 | metrics.update({"quantstats": qs_metrics}) 159 | 160 | return metrics -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | # QuantDesk Environment Configuration Template 2 | # Copy this file to .env and fill in your actual values 3 | # DO NOT commit .env to version control 4 | 5 | # ============================================================================= 6 | # BROKER API CREDENTIALS 7 | # ============================================================================= 8 | 9 | # Alpaca Trading API (Paper Trading) 10 | # Get your keys from: https://app.alpaca.markets/paper/dashboard/overview 11 | ALPACA_KEY_ID=your_alpaca_key_id_here 12 | ALPACA_SECRET_KEY=your_alpaca_secret_key_here 13 | ALPACA_BASE_URL=https://paper-api.alpaca.markets 14 | 15 | # Binance API (Testnet) 16 | # Get your keys from: https://testnet.binance.vision/ 17 | BINANCE_API_KEY=your_binance_api_key_here 18 | BINANCE_API_SECRET=your_binance_secret_here 19 | BINANCE_TESTNET=true 20 | 21 | # ============================================================================= 22 | # NOTIFICATION SERVICES 23 | # ============================================================================= 24 | 25 | # Discord Webhook for Trading Alerts 26 | # Create webhook in Discord: Server Settings > Integrations > Webhooks 27 | DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your_webhook_url 28 | 29 | # Slack Webhook (Optional) 30 | SLACK_WEBHOOK_URL=https://hooks.slack.com/services/your_slack_webhook 31 | 32 | # ============================================================================= 33 | # DATA PROVIDERS 34 | # ============================================================================= 35 | 36 | # Polygon.io API Key (Optional - for premium data) 37 | POLYGON_API_KEY=your_polygon_api_key_here 38 | 39 | # Alpha Vantage API Key (Optional) 40 | ALPHA_VANTAGE_API_KEY=your_alpha_vantage_key_here 41 | 42 | # ============================================================================= 43 | # INFRASTRUCTURE SERVICES 44 | # ============================================================================= 45 | 46 | # MLflow Tracking Server 47 | MLFLOW_TRACKING_URI=http://mlflow:5000 48 | MLFLOW_EXPERIMENT_NAME=quantdesk-experiments 49 | 50 | # Redis Configuration 51 | REDIS_URL=redis://redis:6379/0 52 | REDIS_PASSWORD= 53 | 54 | # PostgreSQL Database (for production deployments) 55 | DATABASE_URL=postgresql://postgres:password@localhost:5432/quantdesk 56 | POSTGRES_USER=postgres 57 | POSTGRES_PASSWORD=your_secure_password_here 58 | POSTGRES_DB=quantdesk 59 | 60 | # ============================================================================= 61 | # SECURITY & AUTHENTICATION 62 | # ============================================================================= 63 | 64 | # API Security 65 | SECRET_KEY=your_very_secure_secret_key_here_min_32_chars 66 | API_KEY=your_api_key_for_quantdesk_endpoints 67 | 68 | # JWT Configuration 69 | JWT_SECRET_KEY=your_jwt_secret_key_here 70 | JWT_ALGORITHM=HS256 71 | JWT_EXPIRATION_HOURS=24 72 | 73 | # ============================================================================= 74 | # TRADING CONFIGURATION 75 | # ============================================================================= 76 | 77 | # Risk Management 78 | MAX_POSITION_SIZE=10000 # Maximum position size in USD 79 | MAX_DAILY_LOSS=1000 # Maximum daily loss in USD 80 | MAX_DRAWDOWN=0.15 # Maximum portfolio drawdown (15%) 81 | VOLATILITY_TARGET=0.10 # Target annual volatility (10%) 82 | 83 | # Trading Hours (UTC) 84 | MARKET_OPEN_HOUR=13 # 9:30 AM EST = 13:30 UTC (during standard time) 85 | MARKET_CLOSE_HOUR=20 # 4:00 PM EST = 20:00 UTC 86 | 87 | # Paper Trading Settings 88 | INITIAL_CAPITAL=100000 # Starting capital in USD 89 | COMMISSION_PER_SHARE=0.005 # Commission per share 90 | SLIPPAGE_BPS=5 # Slippage in basis points 91 | 92 | # ============================================================================= 93 | # LOGGING & MONITORING 94 | # ============================================================================= 95 | 96 | # Log Level (DEBUG, INFO, WARNING, ERROR, CRITICAL) 97 | LOG_LEVEL=INFO 98 | 99 | # Structured Logging 100 | LOG_FORMAT=json 101 | LOG_FILE_PATH=logs/quantdesk.log 102 | 103 | # Prometheus Metrics 104 | PROMETHEUS_PORT=8001 105 | METRICS_ENABLED=true 106 | 107 | # ============================================================================= 108 | # DEVELOPMENT SETTINGS 109 | # ============================================================================= 110 | 111 | # Environment Type 112 | ENVIRONMENT=development # development, staging, production 113 | 114 | # Debug Mode 115 | DEBUG=true 116 | 117 | # API Settings 118 | API_HOST=0.0.0.0 119 | API_PORT=8000 120 | API_RELOAD=true # Auto-reload on code changes (dev only) 121 | 122 | # Data Cache Settings 123 | CACHE_DIR=data/cache 124 | CACHE_EXPIRY_HOURS=24 125 | 126 | # ============================================================================= 127 | # BACKTEST CONFIGURATION 128 | # ============================================================================= 129 | 130 | # Default Backtest Settings 131 | BACKTEST_START_DATE=2020-01-01 132 | BACKTEST_END_DATE=2024-12-31 133 | BACKTEST_INITIAL_CASH=100000 134 | BACKTEST_COMMISSION=0.001 135 | 136 | # Walk-Forward Analysis 137 | WF_TRAIN_PERIOD_DAYS=252 # 1 year training 138 | WF_TEST_PERIOD_DAYS=63 # 1 quarter testing 139 | WF_STEP_DAYS=21 # 1 month steps 140 | 141 | # ============================================================================= 142 | # STRATEGY PARAMETERS 143 | # ============================================================================= 144 | 145 | # Mean Reversion Strategy 146 | MR_LOOKBACK_PERIOD=60 147 | MR_ZSCORE_THRESHOLD=2.0 148 | MR_MAX_POSITIONS=3 149 | 150 | # Momentum Strategy 151 | MOM_LOOKBACK_PERIOD=20 152 | MOM_TOP_N_ASSETS=5 153 | MOM_REBALANCE_FREQUENCY=daily 154 | 155 | # Volatility Breakout Strategy 156 | VB_ATR_PERIOD=20 157 | VB_ATR_MULTIPLIER=2.0 158 | VB_PYRAMID_MAX=3 159 | 160 | # ============================================================================= 161 | # EXTERNAL INTEGRATIONS 162 | # ============================================================================= 163 | 164 | # Email Notifications (Optional) 165 | SMTP_HOST=smtp.gmail.com 166 | SMTP_PORT=587 167 | SMTP_USERNAME=your_email@gmail.com 168 | SMTP_PASSWORD=your_app_password 169 | EMAIL_FROM=quantdesk@yourdomain.com 170 | EMAIL_TO=alerts@yourdomain.com 171 | 172 | # Telegram Bot (Optional) 173 | TELEGRAM_BOT_TOKEN=your_telegram_bot_token 174 | TELEGRAM_CHAT_ID=your_telegram_chat_id 175 | 176 | # ============================================================================= 177 | # CLOUD DEPLOYMENT (Optional) 178 | # ============================================================================= 179 | 180 | # AWS Configuration 181 | AWS_ACCESS_KEY_ID=your_aws_access_key 182 | AWS_SECRET_ACCESS_KEY=your_aws_secret_key 183 | AWS_DEFAULT_REGION=us-east-1 184 | S3_BUCKET_NAME=quantdesk-data 185 | 186 | # Docker Registry 187 | DOCKER_REGISTRY=ghcr.io 188 | DOCKER_IMAGE_NAME=quantdesk/quantdesk 189 | -------------------------------------------------------------------------------- /algos/position_unwinding/config.yaml: -------------------------------------------------------------------------------- 1 | name: "Institutional Position Unwinding" 2 | description: "Advanced position unwinding strategies for large institutional positions" 3 | version: "1.0.0" 4 | author: "QuantDesk Team" 5 | 6 | # Strategy parameters 7 | strategy: 8 | class: "algos.position_unwinding.strategy.PositionUnwindingStrategy" 9 | params: 10 | unwind_method: "adaptive" # twap, vwap, iceberg, is, adaptive 11 | target_position: 0 # Target position (0 = full unwind) 12 | max_participation_rate: 0.20 # Max % of volume to consume 13 | min_participation_rate: 0.05 # Min % of volume to consume 14 | time_horizon: 240 # Minutes to complete unwinding 15 | iceberg_show_size: 0.1 # % of order to show in iceberg orders 16 | volatility_threshold: 0.02 # Pause if volatility exceeds 2% 17 | liquidity_buffer: 0.3 # Reserve 30% of ADV for liquidity 18 | risk_factor: 0.5 # Risk aversion (0=aggressive, 1=conservative) 19 | rebalance_frequency: 5 # Minutes between rebalancing 20 | max_order_value: 50000 # Maximum single order value USD 21 | stealth_mode: true # Use randomization to avoid detection 22 | dark_pool_preference: 0.7 # Preference for dark pools (0-1) 23 | printlog: false 24 | 25 | # Execution profiles for different scenarios 26 | execution_profiles: 27 | conservative: 28 | max_participation_rate: 0.10 29 | min_participation_rate: 0.03 30 | time_horizon: 480 # 8 hours 31 | risk_factor: 0.8 32 | volatility_threshold: 0.015 33 | stealth_mode: true 34 | 35 | aggressive: 36 | max_participation_rate: 0.30 37 | min_participation_rate: 0.10 38 | time_horizon: 120 # 2 hours 39 | risk_factor: 0.2 40 | volatility_threshold: 0.03 41 | stealth_mode: false 42 | 43 | stealth: 44 | max_participation_rate: 0.08 45 | min_participation_rate: 0.02 46 | time_horizon: 720 # 12 hours 47 | risk_factor: 0.9 48 | volatility_threshold: 0.01 49 | stealth_mode: true 50 | dark_pool_preference: 0.9 51 | 52 | # Algorithm-specific parameters 53 | algorithms: 54 | twap: 55 | name: "Time-Weighted Average Price" 56 | description: "Spreads execution evenly across time" 57 | best_for: "Stable markets with predictable volume" 58 | 59 | vwap: 60 | name: "Volume-Weighted Average Price" 61 | description: "Executes proportional to historical volume" 62 | best_for: "Markets with strong volume patterns" 63 | volume_lookback: 20 # Days for volume averaging 64 | 65 | iceberg: 66 | name: "Iceberg Orders" 67 | description: "Hides order size by showing small portions" 68 | best_for: "Large orders in liquid markets" 69 | show_size_range: [0.05, 0.15] # Random show size % 70 | refresh_threshold: 0.8 # Refresh when filled % 71 | 72 | implementation_shortfall: 73 | name: "Implementation Shortfall" 74 | description: "Balances market impact vs timing risk" 75 | best_for: "Volatile markets with timing constraints" 76 | impact_model: "sqrt" # sqrt, linear, or ml 77 | 78 | adaptive: 79 | name: "Adaptive Execution" 80 | description: "ML-based dynamic execution optimization" 81 | best_for: "All market conditions" 82 | model_features: 83 | - "spread" 84 | - "depth" 85 | - "volatility" 86 | - "volume_ratio" 87 | - "price_trend" 88 | - "time_of_day" 89 | 90 | # Risk management 91 | risk: 92 | max_position_exposure: 0.20 # Max % of portfolio per position 93 | max_daily_unwind: 0.50 # Max % of position to unwind per day 94 | circuit_breakers: 95 | volatility_spike: 0.05 # Pause if vol > 5% 96 | volume_drought: 0.3 # Pause if volume < 30% of normal 97 | adverse_price_move: 0.03 # Pause if price moves > 3% against 98 | 99 | emergency_liquidation: 100 | enabled: true 101 | trigger_loss: 0.10 # Emergency exit if position down 10% 102 | method: "market" # Use market orders for emergency 103 | max_slippage: 0.02 # Accept up to 2% slippage 104 | 105 | # Market microstructure 106 | microstructure: 107 | tick_size: 0.01 108 | min_order_size: 1 109 | max_order_size: 10000 110 | 111 | venues: 112 | primary_exchange: 0.4 # 40% to primary exchange 113 | dark_pools: 0.4 # 40% to dark pools 114 | ecns: 0.2 # 20% to ECNs 115 | 116 | order_types: 117 | market: 0.1 # 10% market orders 118 | limit: 0.7 # 70% limit orders 119 | hidden: 0.2 # 20% hidden orders 120 | 121 | # Performance benchmarks 122 | benchmarks: 123 | twap_benchmark: true # Compare to TWAP benchmark 124 | vwap_benchmark: true # Compare to VWAP benchmark 125 | arrival_price: true # Compare to arrival price 126 | 127 | target_metrics: 128 | implementation_shortfall: 25 # bps 129 | market_impact: 15 # bps 130 | timing_cost: 10 # bps 131 | completion_rate: 0.95 # 95% completion rate 132 | 133 | # Data requirements 134 | data: 135 | required_history: 30 # Days of history needed 136 | tick_data: false # Use minute bars (not tick data) 137 | level2_data: false # Order book data not required 138 | 139 | required_fields: 140 | - "open" 141 | - "high" 142 | - "low" 143 | - "close" 144 | - "volume" 145 | - "vwap" # If available 146 | 147 | # Monitoring and alerts 148 | monitoring: 149 | real_time_metrics: true 150 | alert_thresholds: 151 | execution_delay: 10 # Alert if execution delayed > 10 min 152 | slippage_threshold: 50 # Alert if slippage > 50 bps 153 | completion_risk: 0.2 # Alert if completion risk > 20% 154 | 155 | reporting_frequency: "5min" 156 | 157 | dashboards: 158 | - "execution_progress" 159 | - "market_impact" 160 | - "risk_monitoring" 161 | - "venue_analysis" 162 | 163 | # Backtesting 164 | backtest: 165 | start_date: "2020-01-01" 166 | end_date: "2024-12-31" 167 | initial_cash: 1000000 168 | commission: 0.005 169 | slippage: 0.001 170 | 171 | scenarios: 172 | - name: "large_tech_unwind" 173 | positions: {"AAPL": 10000, "MSFT": 8000, "GOOGL": 5000} 174 | target: {"AAPL": 0, "MSFT": 0, "GOOGL": 0} 175 | 176 | - name: "partial_portfolio_rebalance" 177 | positions: {"SPY": 20000, "QQQ": 15000} 178 | target: {"SPY": 10000, "QQQ": 7500} 179 | 180 | # Compliance 181 | compliance: 182 | position_limits: true # Respect position limits 183 | concentration_limits: true # Respect concentration limits 184 | market_hours_only: true # Trade only during market hours 185 | 186 | regulations: 187 | - "MiFID II" # Best execution requirements 188 | - "Reg NMS" # Order protection rules 189 | - "MAR" # Market abuse regulation 190 | 191 | audit_trail: true # Maintain full audit trail 192 | best_execution: true # Document best execution -------------------------------------------------------------------------------- /algos/position_unwinding/config.yaml.bak: -------------------------------------------------------------------------------- 1 | name: "Institutional Position Unwinding" 2 | description: "Advanced position unwinding strategies for large institutional positions" 3 | version: "1.0.0" 4 | author: "QuantDesk Team" 5 | 6 | # Strategy parameters 7 | strategy: 8 | class: "algos.position_unwinding.strategy.PositionUnwindingStrategy" 9 | params: 10 | unwind_method: "adaptive" # twap, vwap, iceberg, is, adaptive 11 | target_position: 0 # Target position (0 = full unwind) 12 | max_participation_rate: 0.20 # Max % of volume to consume 13 | min_participation_rate: 0.05 # Min % of volume to consume 14 | time_horizon: 240 # Minutes to complete unwinding 15 | iceberg_show_size: 0.1 # % of order to show in iceberg orders 16 | volatility_threshold: 0.02 # Pause if volatility exceeds 2% 17 | liquidity_buffer: 0.3 # Reserve 30% of ADV for liquidity 18 | risk_factor: 0.5 # Risk aversion (0=aggressive, 1=conservative) 19 | rebalance_frequency: 5 # Minutes between rebalancing 20 | max_order_value: 50000 # Maximum single order value USD 21 | stealth_mode: true # Use randomization to avoid detection 22 | dark_pool_preference: 0.7 # Preference for dark pools (0-1) 23 | printlog: false 24 | 25 | # Execution profiles for different scenarios 26 | execution_profiles: 27 | conservative: 28 | max_participation_rate: 0.10 29 | min_participation_rate: 0.03 30 | time_horizon: 480 # 8 hours 31 | risk_factor: 0.8 32 | volatility_threshold: 0.015 33 | stealth_mode: true 34 | 35 | aggressive: 36 | max_participation_rate: 0.30 37 | min_participation_rate: 0.10 38 | time_horizon: 120 # 2 hours 39 | risk_factor: 0.2 40 | volatility_threshold: 0.03 41 | stealth_mode: false 42 | 43 | stealth: 44 | max_participation_rate: 0.08 45 | min_participation_rate: 0.02 46 | time_horizon: 720 # 12 hours 47 | risk_factor: 0.9 48 | volatility_threshold: 0.01 49 | stealth_mode: true 50 | dark_pool_preference: 0.9 51 | 52 | # Algorithm-specific parameters 53 | algorithms: 54 | twap: 55 | name: "Time-Weighted Average Price" 56 | description: "Spreads execution evenly across time" 57 | best_for: "Stable markets with predictable volume" 58 | 59 | vwap: 60 | name: "Volume-Weighted Average Price" 61 | description: "Executes proportional to historical volume" 62 | best_for: "Markets with strong volume patterns" 63 | volume_lookback: 20 # Days for volume averaging 64 | 65 | iceberg: 66 | name: "Iceberg Orders" 67 | description: "Hides order size by showing small portions" 68 | best_for: "Large orders in liquid markets" 69 | show_size_range: [0.05, 0.15] # Random show size % 70 | refresh_threshold: 0.8 # Refresh when filled % 71 | 72 | implementation_shortfall: 73 | name: "Implementation Shortfall" 74 | description: "Balances market impact vs timing risk" 75 | best_for: "Volatile markets with timing constraints" 76 | impact_model: "sqrt" # sqrt, linear, or ml 77 | 78 | adaptive: 79 | name: "Adaptive Execution" 80 | description: "ML-based dynamic execution optimization" 81 | best_for: "All market conditions" 82 | model_features: 83 | - "spread" 84 | - "depth" 85 | - "volatility" 86 | - "volume_ratio" 87 | - "price_trend" 88 | - "time_of_day" 89 | 90 | # Risk management 91 | risk: 92 | max_position_exposure: 0.20 # Max % of portfolio per position 93 | max_daily_unwind: 0.50 # Max % of position to unwind per day 94 | circuit_breakers: 95 | volatility_spike: 0.05 # Pause if vol > 5% 96 | volume_drought: 0.3 # Pause if volume < 30% of normal 97 | adverse_price_move: 0.03 # Pause if price moves > 3% against 98 | 99 | emergency_liquidation: 100 | enabled: true 101 | trigger_loss: 0.10 # Emergency exit if position down 10% 102 | method: "market" # Use market orders for emergency 103 | max_slippage: 0.02 # Accept up to 2% slippage 104 | 105 | # Market microstructure 106 | microstructure: 107 | tick_size: 0.01 108 | min_order_size: 1 109 | max_order_size: 10000 110 | 111 | venues: 112 | primary_exchange: 0.4 # 40% to primary exchange 113 | dark_pools: 0.4 # 40% to dark pools 114 | ecns: 0.2 # 20% to ECNs 115 | 116 | order_types: 117 | market: 0.1 # 10% market orders 118 | limit: 0.7 # 70% limit orders 119 | hidden: 0.2 # 20% hidden orders 120 | 121 | # Performance benchmarks 122 | benchmarks: 123 | twap_benchmark: true # Compare to TWAP benchmark 124 | vwap_benchmark: true # Compare to VWAP benchmark 125 | arrival_price: true # Compare to arrival price 126 | 127 | target_metrics: 128 | implementation_shortfall: 25 # bps 129 | market_impact: 15 # bps 130 | timing_cost: 10 # bps 131 | completion_rate: 0.95 # 95% completion rate 132 | 133 | # Data requirements 134 | data: 135 | required_history: 30 # Days of history needed 136 | tick_data: false # Use minute bars (not tick data) 137 | level2_data: false # Order book data not required 138 | 139 | required_fields: 140 | - "open" 141 | - "high" 142 | - "low" 143 | - "close" 144 | - "volume" 145 | - "vwap" # If available 146 | 147 | # Monitoring and alerts 148 | monitoring: 149 | real_time_metrics: true 150 | alert_thresholds: 151 | execution_delay: 10 # Alert if execution delayed > 10 min 152 | slippage_threshold: 50 # Alert if slippage > 50 bps 153 | completion_risk: 0.2 # Alert if completion risk > 20% 154 | 155 | reporting_frequency: "5min" 156 | 157 | dashboards: 158 | - "execution_progress" 159 | - "market_impact" 160 | - "risk_monitoring" 161 | - "venue_analysis" 162 | 163 | # Backtesting 164 | backtest: 165 | start_date: "2020-01-01" 166 | end_date: "2024-12-31" 167 | initial_cash: 1000000 168 | commission: 0.005 169 | slippage: 0.001 170 | 171 | scenarios: 172 | - name: "large_tech_unwind" 173 | positions: {"AAPL": 10000, "MSFT": 8000, "GOOGL": 5000} 174 | target: {"AAPL": 0, "MSFT": 0, "GOOGL": 0} 175 | 176 | - name: "partial_portfolio_rebalance" 177 | positions: {"SPY": 20000, "QQQ": 15000} 178 | target: {"SPY": 10000, "QQQ": 7500} 179 | 180 | # Compliance 181 | compliance: 182 | position_limits: true # Respect position limits 183 | concentration_limits: true # Respect concentration limits 184 | market_hours_only: true # Trade only during market hours 185 | 186 | regulations: 187 | - "MiFID II" # Best execution requirements 188 | - "Reg NMS" # Order protection rules 189 | - "MAR" # Market abuse regulation 190 | 191 | audit_trail: true # Maintain full audit trail 192 | best_execution: true # Document best execution -------------------------------------------------------------------------------- /scripts/ingest_alpaca.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Stream Alpaca market data and save to parquet files.""" 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | import signal 7 | import sys 8 | from datetime import datetime, timezone 9 | from pathlib import Path 10 | from typing import Any 11 | 12 | import click 13 | import pandas as pd 14 | from alpaca.data.live import StockDataStream 15 | from alpaca.data.models import Bar, Quote, Trade 16 | 17 | from quantdesk.utils.env import SETTINGS 18 | from quantdesk.utils.logging import get_logger 19 | 20 | log = get_logger(__name__) 21 | 22 | 23 | class AlpacaDataIngester: 24 | """Real-time Alpaca data ingestion with parquet storage.""" 25 | 26 | def __init__(self, output_dir: str = "data/cache/alpaca") -> None: 27 | """Initialize the data ingester. 28 | 29 | :param output_dir: Directory to save parquet files. 30 | """ 31 | self.output_dir = Path(output_dir) 32 | self.output_dir.mkdir(parents=True, exist_ok=True) 33 | 34 | self.stream = StockDataStream( 35 | api_key=SETTINGS.alpaca_key, 36 | secret_key=SETTINGS.alpaca_secret, 37 | ) 38 | 39 | self.data_buffer: dict[str, list[dict[str, Any]]] = { 40 | "bars": [], 41 | "quotes": [], 42 | "trades": [], 43 | } 44 | self.buffer_size = 1000 45 | self.running = False 46 | 47 | async def handle_bar(self, bar: Bar) -> None: 48 | """Handle incoming bar data.""" 49 | data = { 50 | "timestamp": bar.timestamp, 51 | "symbol": bar.symbol, 52 | "open": float(bar.open), 53 | "high": float(bar.high), 54 | "low": float(bar.low), 55 | "close": float(bar.close), 56 | "volume": int(bar.volume), 57 | "trade_count": int(bar.trade_count) if bar.trade_count else 0, 58 | "vwap": float(bar.vwap) if bar.vwap else None, 59 | } 60 | 61 | self.data_buffer["bars"].append(data) 62 | log.debug("bar.received", symbol=bar.symbol, timestamp=bar.timestamp) 63 | 64 | if len(self.data_buffer["bars"]) >= self.buffer_size: 65 | await self._flush_buffer("bars") 66 | 67 | async def handle_quote(self, quote: Quote) -> None: 68 | """Handle incoming quote data.""" 69 | data = { 70 | "timestamp": quote.timestamp, 71 | "symbol": quote.symbol, 72 | "bid_price": float(quote.bid_price) if quote.bid_price else None, 73 | "bid_size": int(quote.bid_size) if quote.bid_size else None, 74 | "ask_price": float(quote.ask_price) if quote.ask_price else None, 75 | "ask_size": int(quote.ask_size) if quote.ask_size else None, 76 | } 77 | 78 | self.data_buffer["quotes"].append(data) 79 | 80 | if len(self.data_buffer["quotes"]) >= self.buffer_size: 81 | await self._flush_buffer("quotes") 82 | 83 | async def handle_trade(self, trade: Trade) -> None: 84 | """Handle incoming trade data.""" 85 | data = { 86 | "timestamp": trade.timestamp, 87 | "symbol": trade.symbol, 88 | "price": float(trade.price), 89 | "size": int(trade.size), 90 | } 91 | 92 | self.data_buffer["trades"].append(data) 93 | 94 | if len(self.data_buffer["trades"]) >= self.buffer_size: 95 | await self._flush_buffer("trades") 96 | 97 | async def _flush_buffer(self, data_type: str) -> None: 98 | """Flush buffer to parquet file.""" 99 | if not self.data_buffer[data_type]: 100 | return 101 | 102 | df = pd.DataFrame(self.data_buffer[data_type]) 103 | df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True) 104 | 105 | # Create filename with current date 106 | date_str = datetime.now(timezone.utc).strftime("%Y%m%d") 107 | filename = self.output_dir / f"{data_type}_{date_str}.parquet" 108 | 109 | # Append to existing file or create new one 110 | if filename.exists(): 111 | existing_df = pd.read_parquet(filename) 112 | df = pd.concat([existing_df, df], ignore_index=True) 113 | 114 | df.to_parquet(filename, index=False) 115 | 116 | log.info( 117 | "data.flushed", 118 | data_type=data_type, 119 | records=len(self.data_buffer[data_type]), 120 | filename=str(filename), 121 | ) 122 | 123 | # Clear buffer 124 | self.data_buffer[data_type] = [] 125 | 126 | async def start_streaming(self, symbols: list[str]) -> None: 127 | """Start streaming data for given symbols.""" 128 | log.info("ingester.starting", symbols=symbols) 129 | 130 | # Register handlers 131 | self.stream.subscribe_bars(self.handle_bar, *symbols) 132 | self.stream.subscribe_quotes(self.handle_quote, *symbols) 133 | self.stream.subscribe_trades(self.handle_trade, *symbols) 134 | 135 | self.running = True 136 | 137 | # Start the stream 138 | await self.stream._run_forever() 139 | 140 | async def stop_streaming(self) -> None: 141 | """Stop streaming and flush remaining data.""" 142 | log.info("ingester.stopping") 143 | self.running = False 144 | 145 | # Flush remaining data 146 | for data_type in self.data_buffer: 147 | await self._flush_buffer(data_type) 148 | 149 | await self.stream.stop_ws() 150 | log.info("ingester.stopped") 151 | 152 | 153 | # Global ingester instance for signal handling 154 | ingester: AlpacaDataIngester | None = None 155 | 156 | 157 | def signal_handler(signum: int, frame: Any) -> None: 158 | """Handle shutdown signals gracefully.""" 159 | log.info("signal.received", signal=signum) 160 | if ingester: 161 | asyncio.create_task(ingester.stop_streaming()) 162 | 163 | 164 | @click.command() 165 | @click.option( 166 | "--symbols", 167 | default="SPY,QQQ,AAPL,MSFT,GOOGL", 168 | help="Comma-separated list of symbols to stream", 169 | ) 170 | @click.option( 171 | "--output-dir", 172 | default="data/cache/alpaca", 173 | help="Output directory for parquet files", 174 | ) 175 | def main(symbols: str, output_dir: str) -> None: 176 | """Stream Alpaca market data to parquet files.""" 177 | global ingester 178 | 179 | # Setup signal handlers 180 | signal.signal(signal.SIGINT, signal_handler) 181 | signal.signal(signal.SIGTERM, signal_handler) 182 | 183 | # Parse symbols 184 | symbol_list = [s.strip().upper() for s in symbols.split(",")] 185 | 186 | # Create ingester 187 | ingester = AlpacaDataIngester(output_dir) 188 | 189 | # Start streaming 190 | try: 191 | asyncio.run(ingester.start_streaming(symbol_list)) 192 | except KeyboardInterrupt: 193 | log.info("ingester.interrupted") 194 | except Exception: 195 | log.exception("ingester.error") 196 | sys.exit(1) 197 | 198 | 199 | if __name__ == "__main__": 200 | main() -------------------------------------------------------------------------------- /scripts/ingest_alpaca.py.bak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Stream Alpaca market data and save to parquet files.""" 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | import signal 7 | import sys 8 | from datetime import datetime, timezone 9 | from pathlib import Path 10 | from typing import Any 11 | 12 | import click 13 | import pandas as pd 14 | from alpaca.data.live import StockDataStream 15 | from alpaca.data.models import Bar, Quote, Trade 16 | 17 | from quantdesk.utils.env import SETTINGS 18 | from quantdesk.utils.logging import get_logger 19 | 20 | log = get_logger(__name__) 21 | 22 | 23 | class AlpacaDataIngester: 24 | """Real-time Alpaca data ingestion with parquet storage.""" 25 | 26 | def __init__(self, output_dir: str = "data/cache/alpaca") -> None: 27 | """Initialize the data ingester. 28 | 29 | :param output_dir: Directory to save parquet files. 30 | """ 31 | self.output_dir = Path(output_dir) 32 | self.output_dir.mkdir(parents=True, exist_ok=True) 33 | 34 | self.stream = StockDataStream( 35 | api_key=SETTINGS.alpaca_key, 36 | secret_key=SETTINGS.alpaca_secret, 37 | ) 38 | 39 | self.data_buffer: dict[str, list[dict[str, Any]]] = { 40 | "bars": [], 41 | "quotes": [], 42 | "trades": [], 43 | } 44 | self.buffer_size = 1000 45 | self.running = False 46 | 47 | async def handle_bar(self, bar: Bar) -> None: 48 | """Handle incoming bar data.""" 49 | data = { 50 | "timestamp": bar.timestamp, 51 | "symbol": bar.symbol, 52 | "open": float(bar.open), 53 | "high": float(bar.high), 54 | "low": float(bar.low), 55 | "close": float(bar.close), 56 | "volume": int(bar.volume), 57 | "trade_count": int(bar.trade_count) if bar.trade_count else 0, 58 | "vwap": float(bar.vwap) if bar.vwap else None, 59 | } 60 | 61 | self.data_buffer["bars"].append(data) 62 | log.debug("bar.received", symbol=bar.symbol, timestamp=bar.timestamp) 63 | 64 | if len(self.data_buffer["bars"]) >= self.buffer_size: 65 | await self._flush_buffer("bars") 66 | 67 | async def handle_quote(self, quote: Quote) -> None: 68 | """Handle incoming quote data.""" 69 | data = { 70 | "timestamp": quote.timestamp, 71 | "symbol": quote.symbol, 72 | "bid_price": float(quote.bid_price) if quote.bid_price else None, 73 | "bid_size": int(quote.bid_size) if quote.bid_size else None, 74 | "ask_price": float(quote.ask_price) if quote.ask_price else None, 75 | "ask_size": int(quote.ask_size) if quote.ask_size else None, 76 | } 77 | 78 | self.data_buffer["quotes"].append(data) 79 | 80 | if len(self.data_buffer["quotes"]) >= self.buffer_size: 81 | await self._flush_buffer("quotes") 82 | 83 | async def handle_trade(self, trade: Trade) -> None: 84 | """Handle incoming trade data.""" 85 | data = { 86 | "timestamp": trade.timestamp, 87 | "symbol": trade.symbol, 88 | "price": float(trade.price), 89 | "size": int(trade.size), 90 | } 91 | 92 | self.data_buffer["trades"].append(data) 93 | 94 | if len(self.data_buffer["trades"]) >= self.buffer_size: 95 | await self._flush_buffer("trades") 96 | 97 | async def _flush_buffer(self, data_type: str) -> None: 98 | """Flush buffer to parquet file.""" 99 | if not self.data_buffer[data_type]: 100 | return 101 | 102 | df = pd.DataFrame(self.data_buffer[data_type]) 103 | df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True) 104 | 105 | # Create filename with current date 106 | date_str = datetime.now(timezone.utc).strftime("%Y%m%d") 107 | filename = self.output_dir / f"{data_type}_{date_str}.parquet" 108 | 109 | # Append to existing file or create new one 110 | if filename.exists(): 111 | existing_df = pd.read_parquet(filename) 112 | df = pd.concat([existing_df, df], ignore_index=True) 113 | 114 | df.to_parquet(filename, index=False) 115 | 116 | log.info( 117 | "data.flushed", 118 | data_type=data_type, 119 | records=len(self.data_buffer[data_type]), 120 | filename=str(filename), 121 | ) 122 | 123 | # Clear buffer 124 | self.data_buffer[data_type] = [] 125 | 126 | async def start_streaming(self, symbols: list[str]) -> None: 127 | """Start streaming data for given symbols.""" 128 | log.info("ingester.starting", symbols=symbols) 129 | 130 | # Register handlers 131 | self.stream.subscribe_bars(self.handle_bar, *symbols) 132 | self.stream.subscribe_quotes(self.handle_quote, *symbols) 133 | self.stream.subscribe_trades(self.handle_trade, *symbols) 134 | 135 | self.running = True 136 | 137 | # Start the stream 138 | await self.stream._run_forever() 139 | 140 | async def stop_streaming(self) -> None: 141 | """Stop streaming and flush remaining data.""" 142 | log.info("ingester.stopping") 143 | self.running = False 144 | 145 | # Flush remaining data 146 | for data_type in self.data_buffer: 147 | await self._flush_buffer(data_type) 148 | 149 | await self.stream.stop_ws() 150 | log.info("ingester.stopped") 151 | 152 | 153 | # Global ingester instance for signal handling 154 | ingester: AlpacaDataIngester | None = None 155 | 156 | 157 | def signal_handler(signum: int, frame: Any) -> None: 158 | """Handle shutdown signals gracefully.""" 159 | log.info("signal.received", signal=signum) 160 | if ingester: 161 | asyncio.create_task(ingester.stop_streaming()) 162 | 163 | 164 | @click.command() 165 | @click.option( 166 | "--symbols", 167 | default="SPY,QQQ,AAPL,MSFT,GOOGL", 168 | help="Comma-separated list of symbols to stream", 169 | ) 170 | @click.option( 171 | "--output-dir", 172 | default="data/cache/alpaca", 173 | help="Output directory for parquet files", 174 | ) 175 | def main(symbols: str, output_dir: str) -> None: 176 | """Stream Alpaca market data to parquet files.""" 177 | global ingester 178 | 179 | # Setup signal handlers 180 | signal.signal(signal.SIGINT, signal_handler) 181 | signal.signal(signal.SIGTERM, signal_handler) 182 | 183 | # Parse symbols 184 | symbol_list = [s.strip().upper() for s in symbols.split(",")] 185 | 186 | # Create ingester 187 | ingester = AlpacaDataIngester(output_dir) 188 | 189 | # Start streaming 190 | try: 191 | asyncio.run(ingester.start_streaming(symbol_list)) 192 | except KeyboardInterrupt: 193 | log.info("ingester.interrupted") 194 | except Exception: 195 | log.exception("ingester.error") 196 | sys.exit(1) 197 | 198 | 199 | if __name__ == "__main__": 200 | main() -------------------------------------------------------------------------------- /algos/mean_reversion_sp100/strategy.py: -------------------------------------------------------------------------------- 1 | """Mean Reversion Strategy for S&P 100 constituents.""" 2 | from __future__ import annotations 3 | 4 | import backtrader as bt 5 | import numpy as np 6 | import pandas as pd 7 | 8 | from quantdesk.stratlib.base_strategy import BaseStrategy 9 | from quantdesk.stratlib.utils import position_size 10 | from quantdesk.core.risk import kelly_fraction 11 | 12 | 13 | class MeanReversionSP100(BaseStrategy): 14 | """Z-score based mean reversion strategy for S&P 100 stocks. 15 | 16 | Strategy Logic: 17 | - Calculate 5-minute return z-score vs 60-minute lookback 18 | - Enter when |z-score| > threshold (default 2.0) 19 | - Exit when z-score crosses back to mean 20 | - Maximum 3 concurrent positions 21 | - Position sizing based on inverse Kelly criterion 22 | """ 23 | 24 | params = { 25 | "lookback_period": 60, # Minutes for z-score calculation 26 | "return_period": 5, # Minutes for return calculation 27 | "zscore_threshold": 2.0, # Entry threshold 28 | "max_positions": 3, # Maximum concurrent positions 29 | "volatility_target": 0.10, # Annual volatility target 30 | "kelly_fraction": 0.25, # Maximum Kelly fraction 31 | "stop_loss": 0.03, # Stop loss as fraction 32 | "take_profit": 0.015, # Take profit as fraction 33 | "printlog": False, 34 | } 35 | 36 | def __init__(self) -> None: 37 | super().__init__() 38 | 39 | # Strategy-specific indicators 40 | self.returns = bt.indicators.PercentChange( 41 | self.datas[0].close, period=self.params["return_period"] 42 | ) 43 | 44 | # Rolling statistics for z-score 45 | self.returns_mean = bt.indicators.SMA( 46 | self.returns, period=self.params["lookback_period"] 47 | ) 48 | self.returns_std = bt.indicators.StandardDeviation( 49 | self.returns, period=self.params["lookback_period"] 50 | ) 51 | 52 | # Z-score indicator 53 | self.zscore = (self.returns - self.returns_mean) / self.returns_std 54 | 55 | # Position tracking 56 | self.position_count = 0 57 | self.entry_prices: dict[str, float] = {} 58 | self.entry_signals: dict[str, float] = {} 59 | 60 | def next(self) -> None: 61 | """Execute strategy logic on each bar.""" 62 | if self.order: # Skip if order pending 63 | return 64 | 65 | current_price = self.datas[0].close[0] 66 | current_zscore = self.zscore[0] 67 | symbol = self.datas[0]._name 68 | 69 | # Check for exit conditions first 70 | if self.position: 71 | self._check_exit_conditions(current_price, current_zscore, symbol) 72 | return 73 | 74 | # Check for entry conditions 75 | if self.position_count < self.params["max_positions"]: 76 | self._check_entry_conditions(current_price, current_zscore, symbol) 77 | 78 | def _check_entry_conditions(self, price: float, zscore: float, symbol: str) -> None: 79 | """Check if we should enter a position.""" 80 | # Ensure we have enough data 81 | if len(self.zscore) < self.params["lookback_period"]: 82 | return 83 | 84 | # Check if z-score exceeds threshold 85 | if abs(zscore) > self.params["zscore_threshold"]: 86 | # Determine direction (mean reversion) 87 | if zscore > self.params["zscore_threshold"]: 88 | # Price is too high, expect reversion down -> SHORT 89 | direction = -1 90 | elif zscore < -self.params["zscore_threshold"]: 91 | # Price is too low, expect reversion up -> LONG 92 | direction = 1 93 | else: 94 | return 95 | 96 | # Calculate position size 97 | size = self._calculate_position_size(price, direction) 98 | 99 | if size > 0: 100 | if direction == 1: 101 | self.order = self.buy(size=size) 102 | else: 103 | self.order = self.sell(size=size) 104 | 105 | # Track entry 106 | self.entry_prices[symbol] = price 107 | self.entry_signals[symbol] = zscore 108 | self.position_count += 1 109 | 110 | self.log.info( 111 | "entry.signal", 112 | symbol=symbol, 113 | direction="LONG" if direction == 1 else "SHORT", 114 | price=price, 115 | zscore=zscore, 116 | size=size, 117 | ) 118 | 119 | def _check_exit_conditions(self, price: float, zscore: float, symbol: str) -> None: 120 | """Check if we should exit current position.""" 121 | if not self.position: 122 | return 123 | 124 | entry_price = self.entry_prices.get(symbol, 0) 125 | entry_zscore = self.entry_signals.get(symbol, 0) 126 | 127 | if entry_price == 0: 128 | return 129 | 130 | # Calculate P&L 131 | if self.position.size > 0: # Long position 132 | pnl_pct = (price - entry_price) / entry_price 133 | # Exit if z-score crosses back toward mean or stop/profit hit 134 | exit_signal = ( 135 | zscore <= 0 or # Z-score crossed to negative (mean reversion) 136 | pnl_pct <= -self.params["stop_loss"] or 137 | pnl_pct >= self.params["take_profit"] 138 | ) 139 | else: # Short position 140 | pnl_pct = (entry_price - price) / entry_price 141 | # Exit if z-score crosses back toward mean or stop/profit hit 142 | exit_signal = ( 143 | zscore >= 0 or # Z-score crossed to positive (mean reversion) 144 | pnl_pct <= -self.params["stop_loss"] or 145 | pnl_pct >= self.params["take_profit"] 146 | ) 147 | 148 | if exit_signal: 149 | self.order = self.close() 150 | 151 | # Clean up tracking 152 | if symbol in self.entry_prices: 153 | del self.entry_prices[symbol] 154 | if symbol in self.entry_signals: 155 | del self.entry_signals[symbol] 156 | self.position_count = max(0, self.position_count - 1) 157 | 158 | self.log.info( 159 | "exit.signal", 160 | symbol=symbol, 161 | price=price, 162 | zscore=zscore, 163 | pnl_pct=pnl_pct, 164 | reason="mean_reversion" if abs(zscore) < 1.0 else "stop_profit", 165 | ) 166 | 167 | def _calculate_position_size(self, price: float, direction: int) -> int: 168 | """Calculate position size using volatility targeting and Kelly criterion.""" 169 | if len(self.returns_std) < self.params["lookback_period"]: 170 | return 0 171 | 172 | # Get recent volatility estimate (annualized) 173 | daily_vol = self.returns_std[0] * np.sqrt(252 * 24 * 60 / self.params["return_period"]) 174 | 175 | if daily_vol <= 0: 176 | return 0 177 | 178 | # Calculate base position size using volatility targeting 179 | portfolio_value = self.broker.getvalue() 180 | target_position_value = portfolio_value * self.params["volatility_target"] / daily_vol 181 | 182 | # Apply Kelly fraction cap 183 | max_kelly_value = portfolio_value * self.params["kelly_fraction"] 184 | position_value = min(target_position_value, max_kelly_value) 185 | 186 | # Convert to shares 187 | shares = int(position_value / price) 188 | 189 | # Ensure we don't exceed broker limits 190 | max_shares = int(portfolio_value * 0.95 / price) # 95% of portfolio max 191 | shares = min(shares, max_shares) 192 | 193 | return max(shares, 0) 194 | 195 | def notify_order(self, order: bt.Order) -> None: 196 | """Override to track order completion.""" 197 | super().notify_order(order) 198 | 199 | # Reset order reference when completed or failed 200 | if order.status in (order.Completed, order.Canceled, order.Margin, order.Rejected): 201 | self.order = None 202 | 203 | def notify_trade(self, trade: bt.Trade) -> None: 204 | """Override to log trade completion.""" 205 | super().notify_trade(trade) 206 | 207 | if trade.isclosed: 208 | # Additional strategy-specific logging 209 | win_rate = self._calculate_win_rate() 210 | self.log.info( 211 | "trade.stats", 212 | total_trades=len(self.broker.get_orders_open()) + 1, 213 | win_rate=win_rate, 214 | avg_position_count=self.position_count, 215 | ) 216 | 217 | def _calculate_win_rate(self) -> float: 218 | """Calculate current win rate from completed trades.""" 219 | # This is a simplified calculation 220 | # In production, you'd track this more carefully 221 | return 0.54 # Placeholder based on strategy expectations -------------------------------------------------------------------------------- /algos/mean_reversion_sp100/strategy.py.bak: -------------------------------------------------------------------------------- 1 | """Mean Reversion Strategy for S&P 100 constituents.""" 2 | from __future__ import annotations 3 | 4 | import backtrader as bt 5 | import numpy as np 6 | import pandas as pd 7 | 8 | from quantdesk.stratlib.base_strategy import BaseStrategy 9 | from quantdesk.stratlib.utils import position_size 10 | from quantdesk.core.risk import kelly_fraction 11 | 12 | 13 | class MeanReversionSP100(BaseStrategy): 14 | """Z-score based mean reversion strategy for S&P 100 stocks. 15 | 16 | Strategy Logic: 17 | - Calculate 5-minute return z-score vs 60-minute lookback 18 | - Enter when |z-score| > threshold (default 2.0) 19 | - Exit when z-score crosses back to mean 20 | - Maximum 3 concurrent positions 21 | - Position sizing based on inverse Kelly criterion 22 | """ 23 | 24 | params = { 25 | "lookback_period": 60, # Minutes for z-score calculation 26 | "return_period": 5, # Minutes for return calculation 27 | "zscore_threshold": 2.0, # Entry threshold 28 | "max_positions": 3, # Maximum concurrent positions 29 | "volatility_target": 0.10, # Annual volatility target 30 | "kelly_fraction": 0.25, # Maximum Kelly fraction 31 | "stop_loss": 0.03, # Stop loss as fraction 32 | "take_profit": 0.015, # Take profit as fraction 33 | "printlog": False, 34 | } 35 | 36 | def __init__(self) -> None: 37 | super().__init__() 38 | 39 | # Strategy-specific indicators 40 | self.returns = bt.indicators.PercentChange( 41 | self.datas[0].close, period=self.params["return_period"] 42 | ) 43 | 44 | # Rolling statistics for z-score 45 | self.returns_mean = bt.indicators.SMA( 46 | self.returns, period=self.params["lookback_period"] 47 | ) 48 | self.returns_std = bt.indicators.StandardDeviation( 49 | self.returns, period=self.params["lookback_period"] 50 | ) 51 | 52 | # Z-score indicator 53 | self.zscore = (self.returns - self.returns_mean) / self.returns_std 54 | 55 | # Position tracking 56 | self.position_count = 0 57 | self.entry_prices: dict[str, float] = {} 58 | self.entry_signals: dict[str, float] = {} 59 | 60 | def next(self) -> None: 61 | """Execute strategy logic on each bar.""" 62 | if self.order: # Skip if order pending 63 | return 64 | 65 | current_price = self.datas[0].close[0] 66 | current_zscore = self.zscore[0] 67 | symbol = self.datas[0]._name 68 | 69 | # Check for exit conditions first 70 | if self.position: 71 | self._check_exit_conditions(current_price, current_zscore, symbol) 72 | return 73 | 74 | # Check for entry conditions 75 | if self.position_count < self.params["max_positions"]: 76 | self._check_entry_conditions(current_price, current_zscore, symbol) 77 | 78 | def _check_entry_conditions(self, price: float, zscore: float, symbol: str) -> None: 79 | """Check if we should enter a position.""" 80 | # Ensure we have enough data 81 | if len(self.zscore) < self.params["lookback_period"]: 82 | return 83 | 84 | # Check if z-score exceeds threshold 85 | if abs(zscore) > self.params["zscore_threshold"]: 86 | # Determine direction (mean reversion) 87 | if zscore > self.params["zscore_threshold"]: 88 | # Price is too high, expect reversion down -> SHORT 89 | direction = -1 90 | elif zscore < -self.params["zscore_threshold"]: 91 | # Price is too low, expect reversion up -> LONG 92 | direction = 1 93 | else: 94 | return 95 | 96 | # Calculate position size 97 | size = self._calculate_position_size(price, direction) 98 | 99 | if size > 0: 100 | if direction == 1: 101 | self.order = self.buy(size=size) 102 | else: 103 | self.order = self.sell(size=size) 104 | 105 | # Track entry 106 | self.entry_prices[symbol] = price 107 | self.entry_signals[symbol] = zscore 108 | self.position_count += 1 109 | 110 | self.log.info( 111 | "entry.signal", 112 | symbol=symbol, 113 | direction="LONG" if direction == 1 else "SHORT", 114 | price=price, 115 | zscore=zscore, 116 | size=size, 117 | ) 118 | 119 | def _check_exit_conditions(self, price: float, zscore: float, symbol: str) -> None: 120 | """Check if we should exit current position.""" 121 | if not self.position: 122 | return 123 | 124 | entry_price = self.entry_prices.get(symbol, 0) 125 | entry_zscore = self.entry_signals.get(symbol, 0) 126 | 127 | if entry_price == 0: 128 | return 129 | 130 | # Calculate P&L 131 | if self.position.size > 0: # Long position 132 | pnl_pct = (price - entry_price) / entry_price 133 | # Exit if z-score crosses back toward mean or stop/profit hit 134 | exit_signal = ( 135 | zscore <= 0 or # Z-score crossed to negative (mean reversion) 136 | pnl_pct <= -self.params["stop_loss"] or 137 | pnl_pct >= self.params["take_profit"] 138 | ) 139 | else: # Short position 140 | pnl_pct = (entry_price - price) / entry_price 141 | # Exit if z-score crosses back toward mean or stop/profit hit 142 | exit_signal = ( 143 | zscore >= 0 or # Z-score crossed to positive (mean reversion) 144 | pnl_pct <= -self.params["stop_loss"] or 145 | pnl_pct >= self.params["take_profit"] 146 | ) 147 | 148 | if exit_signal: 149 | self.order = self.close() 150 | 151 | # Clean up tracking 152 | if symbol in self.entry_prices: 153 | del self.entry_prices[symbol] 154 | if symbol in self.entry_signals: 155 | del self.entry_signals[symbol] 156 | self.position_count = max(0, self.position_count - 1) 157 | 158 | self.log.info( 159 | "exit.signal", 160 | symbol=symbol, 161 | price=price, 162 | zscore=zscore, 163 | pnl_pct=pnl_pct, 164 | reason="mean_reversion" if abs(zscore) < 1.0 else "stop_profit", 165 | ) 166 | 167 | def _calculate_position_size(self, price: float, direction: int) -> int: 168 | """Calculate position size using volatility targeting and Kelly criterion.""" 169 | if len(self.returns_std) < self.params["lookback_period"]: 170 | return 0 171 | 172 | # Get recent volatility estimate (annualized) 173 | daily_vol = self.returns_std[0] * np.sqrt(252 * 24 * 60 / self.params["return_period"]) 174 | 175 | if daily_vol <= 0: 176 | return 0 177 | 178 | # Calculate base position size using volatility targeting 179 | portfolio_value = self.broker.getvalue() 180 | target_position_value = portfolio_value * self.params["volatility_target"] / daily_vol 181 | 182 | # Apply Kelly fraction cap 183 | max_kelly_value = portfolio_value * self.params["kelly_fraction"] 184 | position_value = min(target_position_value, max_kelly_value) 185 | 186 | # Convert to shares 187 | shares = int(position_value / price) 188 | 189 | # Ensure we don't exceed broker limits 190 | max_shares = int(portfolio_value * 0.95 / price) # 95% of portfolio max 191 | shares = min(shares, max_shares) 192 | 193 | return max(shares, 0) 194 | 195 | def notify_order(self, order: bt.Order) -> None: 196 | """Override to track order completion.""" 197 | super().notify_order(order) 198 | 199 | # Reset order reference when completed or failed 200 | if order.status in (order.Completed, order.Canceled, order.Margin, order.Rejected): 201 | self.order = None 202 | 203 | def notify_trade(self, trade: bt.Trade) -> None: 204 | """Override to log trade completion.""" 205 | super().notify_trade(trade) 206 | 207 | if trade.isclosed: 208 | # Additional strategy-specific logging 209 | win_rate = self._calculate_win_rate() 210 | self.log.info( 211 | "trade.stats", 212 | total_trades=len(self.broker.get_orders_open()) + 1, 213 | win_rate=win_rate, 214 | avg_position_count=self.position_count, 215 | ) 216 | 217 | def _calculate_win_rate(self) -> float: 218 | """Calculate current win rate from completed trades.""" 219 | # This is a simplified calculation 220 | # In production, you'd track this more carefully 221 | return 0.54 # Placeholder based on strategy expectations -------------------------------------------------------------------------------- /algos/position_unwinding/example_usage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Example usage of the position unwinding strategy.""" 3 | from __future__ import annotations 4 | 5 | import backtrader as bt 6 | from datetime import datetime 7 | 8 | from algos.position_unwinding.strategy import PositionUnwindingStrategy 9 | from algos.position_unwinding.execution_analytics import ExecutionAnalytics 10 | from quantdesk.utils.logging import get_logger 11 | 12 | log = get_logger(__name__) 13 | 14 | 15 | def create_sample_data(): 16 | """Create sample data for demonstration.""" 17 | # This would normally come from your data feeds 18 | # For demo purposes, we'll create a simple data feed 19 | data = bt.feeds.YahooFinanceData( 20 | dataname='AAPL', 21 | fromdate=datetime(2024, 1, 1), 22 | todate=datetime(2024, 12, 31), 23 | timeframe=bt.TimeFrame.Minutes, 24 | compression=1 25 | ) 26 | return data 27 | 28 | 29 | def run_twap_unwinding_example(): 30 | """Example of using TWAP unwinding strategy.""" 31 | log.info("example.twap_unwinding.starting") 32 | 33 | cerebro = bt.Cerebro() 34 | 35 | # Add data 36 | data = create_sample_data() 37 | cerebro.adddata(data) 38 | 39 | # Configure TWAP unwinding strategy 40 | cerebro.addstrategy( 41 | PositionUnwindingStrategy, 42 | unwind_method="twap", 43 | target_position=0, # Full unwind 44 | time_horizon=240, # 4 hours 45 | max_participation_rate=0.15, 46 | min_participation_rate=0.05, 47 | stealth_mode=True, 48 | printlog=True 49 | ) 50 | 51 | # Set initial cash and commission 52 | cerebro.broker.setcash(100000.0) 53 | cerebro.broker.setcommission(commission=0.001) 54 | 55 | # Simulate existing position by manually setting 56 | # In practice, this would come from your portfolio 57 | log.info("Simulating large AAPL position that needs unwinding...") 58 | 59 | # Run backtest 60 | results = cerebro.run() 61 | 62 | log.info("example.twap_unwinding.completed") 63 | return results 64 | 65 | 66 | def run_vwap_unwinding_example(): 67 | """Example of using VWAP unwinding strategy.""" 68 | log.info("example.vwap_unwinding.starting") 69 | 70 | cerebro = bt.Cerebro() 71 | 72 | # Add data 73 | data = create_sample_data() 74 | cerebro.adddata(data) 75 | 76 | # Configure VWAP unwinding strategy 77 | cerebro.addstrategy( 78 | PositionUnwindingStrategy, 79 | unwind_method="vwap", 80 | target_position=0, 81 | time_horizon=300, # 5 hours 82 | max_participation_rate=0.20, 83 | min_participation_rate=0.08, 84 | liquidity_buffer=0.25, 85 | stealth_mode=True, 86 | printlog=True 87 | ) 88 | 89 | cerebro.broker.setcash(100000.0) 90 | cerebro.broker.setcommission(commission=0.001) 91 | 92 | results = cerebro.run() 93 | 94 | log.info("example.vwap_unwinding.completed") 95 | return results 96 | 97 | 98 | def run_adaptive_unwinding_example(): 99 | """Example of using adaptive unwinding strategy.""" 100 | log.info("example.adaptive_unwinding.starting") 101 | 102 | cerebro = bt.Cerebro() 103 | 104 | # Add data 105 | data = create_sample_data() 106 | cerebro.adddata(data) 107 | 108 | # Configure adaptive unwinding strategy 109 | cerebro.addstrategy( 110 | PositionUnwindingStrategy, 111 | unwind_method="adaptive", 112 | target_position=0, 113 | time_horizon=180, # 3 hours 114 | max_participation_rate=0.25, 115 | min_participation_rate=0.03, 116 | volatility_threshold=0.015, 117 | risk_factor=0.6, 118 | stealth_mode=True, 119 | dark_pool_preference=0.8, 120 | printlog=True 121 | ) 122 | 123 | cerebro.broker.setcash(100000.0) 124 | cerebro.broker.setcommission(commission=0.001) 125 | 126 | results = cerebro.run() 127 | 128 | log.info("example.adaptive_unwinding.completed") 129 | return results 130 | 131 | 132 | def run_iceberg_unwinding_example(): 133 | """Example of using iceberg order unwinding.""" 134 | log.info("example.iceberg_unwinding.starting") 135 | 136 | cerebro = bt.Cerebro() 137 | 138 | # Add data 139 | data = create_sample_data() 140 | cerebro.adddata(data) 141 | 142 | # Configure iceberg unwinding strategy 143 | cerebro.addstrategy( 144 | PositionUnwindingStrategy, 145 | unwind_method="iceberg", 146 | target_position=0, 147 | time_horizon=360, # 6 hours 148 | iceberg_show_size=0.08, # Show only 8% of order 149 | max_participation_rate=0.18, 150 | stealth_mode=True, 151 | printlog=True 152 | ) 153 | 154 | cerebro.broker.setcash(100000.0) 155 | cerebro.broker.setcommission(commission=0.001) 156 | 157 | results = cerebro.run() 158 | 159 | log.info("example.iceberg_unwinding.completed") 160 | return results 161 | 162 | 163 | def run_conservative_unwinding_example(): 164 | """Example of conservative unwinding profile.""" 165 | log.info("example.conservative_unwinding.starting") 166 | 167 | cerebro = bt.Cerebro() 168 | 169 | # Add data 170 | data = create_sample_data() 171 | cerebro.adddata(data) 172 | 173 | # Configure conservative unwinding (based on config.yaml profile) 174 | cerebro.addstrategy( 175 | PositionUnwindingStrategy, 176 | unwind_method="adaptive", 177 | target_position=0, 178 | time_horizon=480, # 8 hours 179 | max_participation_rate=0.10, 180 | min_participation_rate=0.03, 181 | volatility_threshold=0.015, 182 | risk_factor=0.8, 183 | stealth_mode=True, 184 | dark_pool_preference=0.9, 185 | printlog=True 186 | ) 187 | 188 | cerebro.broker.setcash(100000.0) 189 | cerebro.broker.setcommission(commission=0.001) 190 | 191 | results = cerebro.run() 192 | 193 | log.info("example.conservative_unwinding.completed") 194 | return results 195 | 196 | 197 | def analyze_execution_performance(): 198 | """Example of execution performance analysis.""" 199 | log.info("example.execution_analysis.starting") 200 | 201 | # Create analytics instance 202 | analytics = ExecutionAnalytics() 203 | 204 | # Simulate some execution fills (in practice, these would come from your strategy) 205 | from algos.position_unwinding.execution_analytics import ExecutionFill, ExecutionBenchmark 206 | 207 | # Sample fills for AAPL unwinding 208 | fills = [ 209 | ExecutionFill( 210 | timestamp=datetime(2024, 1, 15, 9, 35), 211 | symbol="AAPL", 212 | side="SELL", 213 | quantity=500, 214 | price=185.25, 215 | venue="dark_pool" 216 | ), 217 | ExecutionFill( 218 | timestamp=datetime(2024, 1, 15, 9, 45), 219 | symbol="AAPL", 220 | side="SELL", 221 | quantity=750, 222 | price=185.10, 223 | venue="primary" 224 | ), 225 | ExecutionFill( 226 | timestamp=datetime(2024, 1, 15, 10, 15), 227 | symbol="AAPL", 228 | side="SELL", 229 | quantity=1000, 230 | price=184.95, 231 | venue="ecn" 232 | ), 233 | ] 234 | 235 | # Add fills to analytics 236 | for fill in fills: 237 | analytics.add_fill(fill) 238 | 239 | # Set benchmark prices 240 | benchmark = ExecutionBenchmark( 241 | arrival_price=185.50, # Price when unwinding started 242 | twap_price=185.15, # TWAP during execution period 243 | vwap_price=185.08, # VWAP during execution period 244 | close_price=184.80 # Close price 245 | ) 246 | analytics.set_benchmark("AAPL", benchmark) 247 | 248 | # Generate execution report 249 | report = analytics.generate_execution_report("AAPL") 250 | 251 | log.info("execution.report", report=report) 252 | 253 | # Calculate specific metrics 254 | is_metrics = analytics.calculate_implementation_shortfall("AAPL") 255 | log.info("implementation_shortfall", metrics=is_metrics) 256 | 257 | venue_breakdown = analytics.calculate_venue_breakdown("AAPL") 258 | log.info("venue_breakdown", breakdown=venue_breakdown) 259 | 260 | # Export to DataFrame for further analysis 261 | df = analytics.export_to_dataframe() 262 | log.info("execution_data_exported", shape=df.shape) 263 | 264 | log.info("example.execution_analysis.completed") 265 | 266 | 267 | def main(): 268 | """Run all unwinding strategy examples.""" 269 | log.info("position_unwinding_examples.starting") 270 | 271 | # Run different unwinding method examples 272 | examples = [ 273 | ("TWAP Unwinding", run_twap_unwinding_example), 274 | ("VWAP Unwinding", run_vwap_unwinding_example), 275 | ("Adaptive Unwinding", run_adaptive_unwinding_example), 276 | ("Iceberg Unwinding", run_iceberg_unwinding_example), 277 | ("Conservative Unwinding", run_conservative_unwinding_example), 278 | ] 279 | 280 | for name, example_func in examples: 281 | try: 282 | log.info(f"Running {name} example...") 283 | example_func() 284 | log.info(f"{name} example completed successfully") 285 | except Exception as e: 286 | log.error(f"Error in {name} example", error=str(e)) 287 | 288 | # Run execution analysis example 289 | try: 290 | log.info("Running execution analysis example...") 291 | analyze_execution_performance() 292 | log.info("Execution analysis example completed successfully") 293 | except Exception as e: 294 | log.error("Error in execution analysis example", error=str(e)) 295 | 296 | log.info("position_unwinding_examples.completed") 297 | 298 | 299 | if __name__ == "__main__": 300 | main() -------------------------------------------------------------------------------- /algos/position_unwinding/example_usage.py.bak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Example usage of the position unwinding strategy.""" 3 | from __future__ import annotations 4 | 5 | import backtrader as bt 6 | from datetime import datetime 7 | 8 | from algos.position_unwinding.strategy import PositionUnwindingStrategy 9 | from algos.position_unwinding.execution_analytics import ExecutionAnalytics 10 | from quantdesk.utils.logging import get_logger 11 | 12 | log = get_logger(__name__) 13 | 14 | 15 | def create_sample_data(): 16 | """Create sample data for demonstration.""" 17 | # This would normally come from your data feeds 18 | # For demo purposes, we'll create a simple data feed 19 | data = bt.feeds.YahooFinanceData( 20 | dataname='AAPL', 21 | fromdate=datetime(2024, 1, 1), 22 | todate=datetime(2024, 12, 31), 23 | timeframe=bt.TimeFrame.Minutes, 24 | compression=1 25 | ) 26 | return data 27 | 28 | 29 | def run_twap_unwinding_example(): 30 | """Example of using TWAP unwinding strategy.""" 31 | log.info("example.twap_unwinding.starting") 32 | 33 | cerebro = bt.Cerebro() 34 | 35 | # Add data 36 | data = create_sample_data() 37 | cerebro.adddata(data) 38 | 39 | # Configure TWAP unwinding strategy 40 | cerebro.addstrategy( 41 | PositionUnwindingStrategy, 42 | unwind_method="twap", 43 | target_position=0, # Full unwind 44 | time_horizon=240, # 4 hours 45 | max_participation_rate=0.15, 46 | min_participation_rate=0.05, 47 | stealth_mode=True, 48 | printlog=True 49 | ) 50 | 51 | # Set initial cash and commission 52 | cerebro.broker.setcash(100000.0) 53 | cerebro.broker.setcommission(commission=0.001) 54 | 55 | # Simulate existing position by manually setting 56 | # In practice, this would come from your portfolio 57 | log.info("Simulating large AAPL position that needs unwinding...") 58 | 59 | # Run backtest 60 | results = cerebro.run() 61 | 62 | log.info("example.twap_unwinding.completed") 63 | return results 64 | 65 | 66 | def run_vwap_unwinding_example(): 67 | """Example of using VWAP unwinding strategy.""" 68 | log.info("example.vwap_unwinding.starting") 69 | 70 | cerebro = bt.Cerebro() 71 | 72 | # Add data 73 | data = create_sample_data() 74 | cerebro.adddata(data) 75 | 76 | # Configure VWAP unwinding strategy 77 | cerebro.addstrategy( 78 | PositionUnwindingStrategy, 79 | unwind_method="vwap", 80 | target_position=0, 81 | time_horizon=300, # 5 hours 82 | max_participation_rate=0.20, 83 | min_participation_rate=0.08, 84 | liquidity_buffer=0.25, 85 | stealth_mode=True, 86 | printlog=True 87 | ) 88 | 89 | cerebro.broker.setcash(100000.0) 90 | cerebro.broker.setcommission(commission=0.001) 91 | 92 | results = cerebro.run() 93 | 94 | log.info("example.vwap_unwinding.completed") 95 | return results 96 | 97 | 98 | def run_adaptive_unwinding_example(): 99 | """Example of using adaptive unwinding strategy.""" 100 | log.info("example.adaptive_unwinding.starting") 101 | 102 | cerebro = bt.Cerebro() 103 | 104 | # Add data 105 | data = create_sample_data() 106 | cerebro.adddata(data) 107 | 108 | # Configure adaptive unwinding strategy 109 | cerebro.addstrategy( 110 | PositionUnwindingStrategy, 111 | unwind_method="adaptive", 112 | target_position=0, 113 | time_horizon=180, # 3 hours 114 | max_participation_rate=0.25, 115 | min_participation_rate=0.03, 116 | volatility_threshold=0.015, 117 | risk_factor=0.6, 118 | stealth_mode=True, 119 | dark_pool_preference=0.8, 120 | printlog=True 121 | ) 122 | 123 | cerebro.broker.setcash(100000.0) 124 | cerebro.broker.setcommission(commission=0.001) 125 | 126 | results = cerebro.run() 127 | 128 | log.info("example.adaptive_unwinding.completed") 129 | return results 130 | 131 | 132 | def run_iceberg_unwinding_example(): 133 | """Example of using iceberg order unwinding.""" 134 | log.info("example.iceberg_unwinding.starting") 135 | 136 | cerebro = bt.Cerebro() 137 | 138 | # Add data 139 | data = create_sample_data() 140 | cerebro.adddata(data) 141 | 142 | # Configure iceberg unwinding strategy 143 | cerebro.addstrategy( 144 | PositionUnwindingStrategy, 145 | unwind_method="iceberg", 146 | target_position=0, 147 | time_horizon=360, # 6 hours 148 | iceberg_show_size=0.08, # Show only 8% of order 149 | max_participation_rate=0.18, 150 | stealth_mode=True, 151 | printlog=True 152 | ) 153 | 154 | cerebro.broker.setcash(100000.0) 155 | cerebro.broker.setcommission(commission=0.001) 156 | 157 | results = cerebro.run() 158 | 159 | log.info("example.iceberg_unwinding.completed") 160 | return results 161 | 162 | 163 | def run_conservative_unwinding_example(): 164 | """Example of conservative unwinding profile.""" 165 | log.info("example.conservative_unwinding.starting") 166 | 167 | cerebro = bt.Cerebro() 168 | 169 | # Add data 170 | data = create_sample_data() 171 | cerebro.adddata(data) 172 | 173 | # Configure conservative unwinding (based on config.yaml profile) 174 | cerebro.addstrategy( 175 | PositionUnwindingStrategy, 176 | unwind_method="adaptive", 177 | target_position=0, 178 | time_horizon=480, # 8 hours 179 | max_participation_rate=0.10, 180 | min_participation_rate=0.03, 181 | volatility_threshold=0.015, 182 | risk_factor=0.8, 183 | stealth_mode=True, 184 | dark_pool_preference=0.9, 185 | printlog=True 186 | ) 187 | 188 | cerebro.broker.setcash(100000.0) 189 | cerebro.broker.setcommission(commission=0.001) 190 | 191 | results = cerebro.run() 192 | 193 | log.info("example.conservative_unwinding.completed") 194 | return results 195 | 196 | 197 | def analyze_execution_performance(): 198 | """Example of execution performance analysis.""" 199 | log.info("example.execution_analysis.starting") 200 | 201 | # Create analytics instance 202 | analytics = ExecutionAnalytics() 203 | 204 | # Simulate some execution fills (in practice, these would come from your strategy) 205 | from algos.position_unwinding.execution_analytics import ExecutionFill, ExecutionBenchmark 206 | 207 | # Sample fills for AAPL unwinding 208 | fills = [ 209 | ExecutionFill( 210 | timestamp=datetime(2024, 1, 15, 9, 35), 211 | symbol="AAPL", 212 | side="SELL", 213 | quantity=500, 214 | price=185.25, 215 | venue="dark_pool" 216 | ), 217 | ExecutionFill( 218 | timestamp=datetime(2024, 1, 15, 9, 45), 219 | symbol="AAPL", 220 | side="SELL", 221 | quantity=750, 222 | price=185.10, 223 | venue="primary" 224 | ), 225 | ExecutionFill( 226 | timestamp=datetime(2024, 1, 15, 10, 15), 227 | symbol="AAPL", 228 | side="SELL", 229 | quantity=1000, 230 | price=184.95, 231 | venue="ecn" 232 | ), 233 | ] 234 | 235 | # Add fills to analytics 236 | for fill in fills: 237 | analytics.add_fill(fill) 238 | 239 | # Set benchmark prices 240 | benchmark = ExecutionBenchmark( 241 | arrival_price=185.50, # Price when unwinding started 242 | twap_price=185.15, # TWAP during execution period 243 | vwap_price=185.08, # VWAP during execution period 244 | close_price=184.80 # Close price 245 | ) 246 | analytics.set_benchmark("AAPL", benchmark) 247 | 248 | # Generate execution report 249 | report = analytics.generate_execution_report("AAPL") 250 | 251 | log.info("execution.report", report=report) 252 | 253 | # Calculate specific metrics 254 | is_metrics = analytics.calculate_implementation_shortfall("AAPL") 255 | log.info("implementation_shortfall", metrics=is_metrics) 256 | 257 | venue_breakdown = analytics.calculate_venue_breakdown("AAPL") 258 | log.info("venue_breakdown", breakdown=venue_breakdown) 259 | 260 | # Export to DataFrame for further analysis 261 | df = analytics.export_to_dataframe() 262 | log.info("execution_data_exported", shape=df.shape) 263 | 264 | log.info("example.execution_analysis.completed") 265 | 266 | 267 | def main(): 268 | """Run all unwinding strategy examples.""" 269 | log.info("position_unwinding_examples.starting") 270 | 271 | # Run different unwinding method examples 272 | examples = [ 273 | ("TWAP Unwinding", run_twap_unwinding_example), 274 | ("VWAP Unwinding", run_vwap_unwinding_example), 275 | ("Adaptive Unwinding", run_adaptive_unwinding_example), 276 | ("Iceberg Unwinding", run_iceberg_unwinding_example), 277 | ("Conservative Unwinding", run_conservative_unwinding_example), 278 | ] 279 | 280 | for name, example_func in examples: 281 | try: 282 | log.info(f"Running {name} example...") 283 | example_func() 284 | log.info(f"{name} example completed successfully") 285 | except Exception as e: 286 | log.error(f"Error in {name} example", error=str(e)) 287 | 288 | # Run execution analysis example 289 | try: 290 | log.info("Running execution analysis example...") 291 | analyze_execution_performance() 292 | log.info("Execution analysis example completed successfully") 293 | except Exception as e: 294 | log.error("Error in execution analysis example", error=str(e)) 295 | 296 | log.info("position_unwinding_examples.completed") 297 | 298 | 299 | if __name__ == "__main__": 300 | main() -------------------------------------------------------------------------------- /algos/position_unwinding/README.md: -------------------------------------------------------------------------------- 1 | # Institutional Position Unwinding Strategies 2 | 3 | This module implements sophisticated position unwinding strategies used by large institutional traders to safely offload large positions without causing significant market impact. 4 | 5 | ## Overview 6 | 7 | When institutional traders need to exit large positions, they can't simply submit market orders as this would cause significant price impact and poor execution quality. Instead, they use advanced execution algorithms that: 8 | 9 | - **Minimize Market Impact**: Break large orders into smaller pieces 10 | - **Optimize Timing**: Execute when liquidity is available 11 | - **Hide Intentions**: Use stealth techniques to avoid detection 12 | - **Balance Risk**: Trade off market impact vs. timing risk 13 | - **Maximize Efficiency**: Achieve best execution across venues 14 | 15 | ## Available Strategies 16 | 17 | ### 1. TWAP (Time-Weighted Average Price) 18 | **Best for**: Stable markets with predictable volume patterns 19 | 20 | Spreads execution evenly across a specified time horizon to minimize timing risk. 21 | 22 | ```python 23 | cerebro.addstrategy( 24 | PositionUnwindingStrategy, 25 | unwind_method="twap", 26 | time_horizon=240, # 4 hours 27 | max_participation_rate=0.15, 28 | stealth_mode=True 29 | ) 30 | ``` 31 | 32 | **Key Features**: 33 | - Linear execution rate 34 | - Predictable completion time 35 | - Low market impact in stable markets 36 | - Good for time-sensitive unwinding 37 | 38 | ### 2. VWAP (Volume-Weighted Average Price) 39 | **Best for**: Markets with strong intraday volume patterns 40 | 41 | Executes proportional to historical volume patterns to minimize market impact. 42 | 43 | ```python 44 | cerebro.addstrategy( 45 | PositionUnwindingStrategy, 46 | unwind_method="vwap", 47 | max_participation_rate=0.20, 48 | liquidity_buffer=0.25 49 | ) 50 | ``` 51 | 52 | **Key Features**: 53 | - Follows natural market rhythm 54 | - Higher execution during high-volume periods 55 | - Adapts to market liquidity 56 | - Excellent for liquid stocks 57 | 58 | ### 3. Iceberg Orders 59 | **Best for**: Very large orders in liquid markets 60 | 61 | Hides order size by only showing small portions to the market. 62 | 63 | ```python 64 | cerebro.addstrategy( 65 | PositionUnwindingStrategy, 66 | unwind_method="iceberg", 67 | iceberg_show_size=0.08, # Show only 8% 68 | stealth_mode=True 69 | ) 70 | ``` 71 | 72 | **Key Features**: 73 | - Conceals true order size 74 | - Prevents front-running 75 | - Reduces adverse selection 76 | - Good for very large positions 77 | 78 | ### 4. Implementation Shortfall 79 | **Best for**: Volatile markets with timing constraints 80 | 81 | Balances market impact costs against timing risk using quantitative optimization. 82 | 83 | ```python 84 | cerebro.addstrategy( 85 | PositionUnwindingStrategy, 86 | unwind_method="is", 87 | risk_factor=0.6, # 0=aggressive, 1=conservative 88 | volatility_threshold=0.02 89 | ) 90 | ``` 91 | 92 | **Key Features**: 93 | - Mathematically optimal execution 94 | - Adapts to market volatility 95 | - Minimizes total implementation cost 96 | - Best for sophisticated traders 97 | 98 | ### 5. Adaptive Execution 99 | **Best for**: All market conditions (recommended) 100 | 101 | Uses machine learning and real-time market analysis for dynamic optimization. 102 | 103 | ```python 104 | cerebro.addstrategy( 105 | PositionUnwindingStrategy, 106 | unwind_method="adaptive", 107 | dark_pool_preference=0.8, 108 | stealth_mode=True 109 | ) 110 | ``` 111 | 112 | **Key Features**: 113 | - Real-time market analysis 114 | - Dynamic participation rates 115 | - Multi-venue optimization 116 | - Continuous learning and adaptation 117 | 118 | ## Execution Profiles 119 | 120 | The strategy includes pre-configured execution profiles for different scenarios: 121 | 122 | ### Conservative Profile 123 | - **Use case**: Risk-averse unwinding, large positions 124 | - **Time horizon**: 8+ hours 125 | - **Participation rate**: 3-10% of volume 126 | - **Stealth mode**: Enabled 127 | - **Dark pool preference**: 90% 128 | 129 | ### Aggressive Profile 130 | - **Use case**: Fast unwinding, time-sensitive 131 | - **Time horizon**: 2 hours 132 | - **Participation rate**: 10-30% of volume 133 | - **Stealth mode**: Disabled 134 | - **Market impact**: Higher but faster completion 135 | 136 | ### Stealth Profile 137 | - **Use case**: Maximum concealment, avoid detection 138 | - **Time horizon**: 12+ hours 139 | - **Participation rate**: 2-8% of volume 140 | - **Randomization**: Maximum 141 | - **Dark pool preference**: 90% 142 | 143 | ## Risk Management Features 144 | 145 | ### Circuit Breakers 146 | The strategy automatically pauses execution when: 147 | - Volatility spikes above threshold (default 2%) 148 | - Volume drops below liquidity buffer (default 30%) 149 | - Adverse price movements occur (default 3%) 150 | 151 | ### Emergency Liquidation 152 | Automatic emergency exit when: 153 | - Position loss exceeds threshold (default 10%) 154 | - Market conditions deteriorate severely 155 | - Completion risk becomes too high 156 | 157 | ## Usage Examples 158 | 159 | ### Basic Usage 160 | ```python 161 | from algos.position_unwinding.strategy import PositionUnwindingStrategy 162 | 163 | # Add to your Backtrader cerebro 164 | cerebro.addstrategy( 165 | PositionUnwindingStrategy, 166 | unwind_method="adaptive", 167 | target_position=0, # Full unwind 168 | time_horizon=240, # 4 hours 169 | stealth_mode=True 170 | ) 171 | ``` 172 | 173 | ### Advanced Configuration 174 | ```python 175 | # Customize for your specific needs 176 | cerebro.addstrategy( 177 | PositionUnwindingStrategy, 178 | unwind_method="vwap", 179 | target_position=5000, # Partial unwind to 5000 shares 180 | max_participation_rate=0.15, 181 | min_participation_rate=0.05, 182 | volatility_threshold=0.025, 183 | liquidity_buffer=0.35, 184 | max_order_value=25000, 185 | dark_pool_preference=0.7, 186 | stealth_mode=True 187 | ) 188 | ``` 189 | 190 | ## Execution Analytics 191 | 192 | Track and analyze execution quality with comprehensive metrics: 193 | 194 | ```python 195 | from algos.position_unwinding.execution_analytics import ExecutionAnalytics 196 | 197 | analytics = ExecutionAnalytics() 198 | # ... add fills ... 199 | report = analytics.generate_execution_report("AAPL") 200 | ``` 201 | 202 | ### Available Metrics 203 | - **Implementation Shortfall**: Total execution cost breakdown 204 | - **Market Impact**: Price impact of your trades 205 | - **Timing Cost**: Opportunity cost of delayed execution 206 | - **Participation Rate**: % of market volume consumed 207 | - **Slippage**: Execution price vs. reference price 208 | - **Venue Analysis**: Performance across different venues 209 | 210 | ## Best Practices 211 | 212 | ### 1. Choose the Right Algorithm 213 | - **Stable markets**: TWAP or VWAP 214 | - **Volatile markets**: Implementation Shortfall or Adaptive 215 | - **Very large orders**: Iceberg or Stealth profile 216 | - **Time-sensitive**: Aggressive profile 217 | - **Risk-averse**: Conservative profile 218 | 219 | ### 2. Parameter Tuning 220 | - **Participation rate**: Start conservative (5-15%) 221 | - **Time horizon**: Allow adequate time (4-8 hours typical) 222 | - **Volatility threshold**: Adjust based on asset volatility 223 | - **Stealth mode**: Enable for large/sensitive positions 224 | 225 | ### 3. Market Conditions 226 | - **Avoid earnings/events**: Higher volatility and impact 227 | - **Use market hours**: Better liquidity and pricing 228 | - **Monitor news flow**: Pause during major news 229 | - **Check correlations**: Consider portfolio-level impact 230 | 231 | ### 4. Venue Selection 232 | - **Dark pools**: 40-70% for large orders 233 | - **Primary exchange**: 20-40% for price discovery 234 | - **ECNs**: 10-30% for additional liquidity 235 | - **Avoid predictable patterns**: Randomize venue selection 236 | 237 | ## Configuration 238 | 239 | Comprehensive configuration is available via `config.yaml`. Key sections: 240 | 241 | - **Strategy parameters**: Execution method, timing, participation rates 242 | - **Risk management**: Circuit breakers, position limits, emergency exits 243 | - **Venue allocation**: Dark pools, exchanges, ECN preferences 244 | - **Monitoring**: Real-time alerts, performance tracking 245 | - **Compliance**: Regulatory requirements, audit trails 246 | 247 | ## Integration 248 | 249 | The unwinding strategies integrate seamlessly with: 250 | - **Backtrader**: Direct strategy integration 251 | - **Portfolio systems**: Position and risk management 252 | - **Broker APIs**: Multi-venue execution 253 | - **Risk systems**: Real-time monitoring and controls 254 | - **Analytics**: Performance measurement and reporting 255 | 256 | ## Performance Benchmarking 257 | 258 | Benchmark your execution against: 259 | - **TWAP**: Time-weighted average price 260 | - **VWAP**: Volume-weighted average price 261 | - **Arrival price**: Price when unwinding started 262 | - **Implementation shortfall**: Industry standard metric 263 | 264 | ## Regulatory Compliance 265 | 266 | Built-in compliance features: 267 | - **Best execution**: Documentation and analysis 268 | - **MiFID II**: European execution requirements 269 | - **Reg NMS**: US order protection rules 270 | - **Audit trails**: Complete execution history 271 | - **Position limits**: Automatic enforcement 272 | 273 | ## Getting Started 274 | 275 | 1. **Install dependencies**: Ensure backtrader, numpy, pandas are available 276 | 2. **Configure strategy**: Choose execution method and parameters 277 | 3. **Set up data feeds**: Minute-level price and volume data 278 | 4. **Initialize positions**: Set starting position and target 279 | 5. **Monitor execution**: Use analytics to track performance 280 | 6. **Analyze results**: Review execution quality metrics 281 | 282 | For detailed examples, see `example_usage.py`. 283 | 284 | ## Support 285 | 286 | For questions about institutional execution strategies or implementation details, consult: 287 | - Academic papers on market microstructure 288 | - Industry best practices from major exchanges 289 | - Regulatory guidance on execution quality 290 | - Professional trading system documentation 291 | 292 | The strategies in this module implement proven institutional techniques used by major hedge funds, banks, and asset managers worldwide. -------------------------------------------------------------------------------- /algos/position_unwinding/README.md.bak: -------------------------------------------------------------------------------- 1 | # Institutional Position Unwinding Strategies 2 | 3 | This module implements sophisticated position unwinding strategies used by large institutional traders to safely offload large positions without causing significant market impact. 4 | 5 | ## Overview 6 | 7 | When institutional traders need to exit large positions, they can't simply submit market orders as this would cause significant price impact and poor execution quality. Instead, they use advanced execution algorithms that: 8 | 9 | - **Minimize Market Impact**: Break large orders into smaller pieces 10 | - **Optimize Timing**: Execute when liquidity is available 11 | - **Hide Intentions**: Use stealth techniques to avoid detection 12 | - **Balance Risk**: Trade off market impact vs. timing risk 13 | - **Maximize Efficiency**: Achieve best execution across venues 14 | 15 | ## Available Strategies 16 | 17 | ### 1. TWAP (Time-Weighted Average Price) 18 | **Best for**: Stable markets with predictable volume patterns 19 | 20 | Spreads execution evenly across a specified time horizon to minimize timing risk. 21 | 22 | ```python 23 | cerebro.addstrategy( 24 | PositionUnwindingStrategy, 25 | unwind_method="twap", 26 | time_horizon=240, # 4 hours 27 | max_participation_rate=0.15, 28 | stealth_mode=True 29 | ) 30 | ``` 31 | 32 | **Key Features**: 33 | - Linear execution rate 34 | - Predictable completion time 35 | - Low market impact in stable markets 36 | - Good for time-sensitive unwinding 37 | 38 | ### 2. VWAP (Volume-Weighted Average Price) 39 | **Best for**: Markets with strong intraday volume patterns 40 | 41 | Executes proportional to historical volume patterns to minimize market impact. 42 | 43 | ```python 44 | cerebro.addstrategy( 45 | PositionUnwindingStrategy, 46 | unwind_method="vwap", 47 | max_participation_rate=0.20, 48 | liquidity_buffer=0.25 49 | ) 50 | ``` 51 | 52 | **Key Features**: 53 | - Follows natural market rhythm 54 | - Higher execution during high-volume periods 55 | - Adapts to market liquidity 56 | - Excellent for liquid stocks 57 | 58 | ### 3. Iceberg Orders 59 | **Best for**: Very large orders in liquid markets 60 | 61 | Hides order size by only showing small portions to the market. 62 | 63 | ```python 64 | cerebro.addstrategy( 65 | PositionUnwindingStrategy, 66 | unwind_method="iceberg", 67 | iceberg_show_size=0.08, # Show only 8% 68 | stealth_mode=True 69 | ) 70 | ``` 71 | 72 | **Key Features**: 73 | - Conceals true order size 74 | - Prevents front-running 75 | - Reduces adverse selection 76 | - Good for very large positions 77 | 78 | ### 4. Implementation Shortfall 79 | **Best for**: Volatile markets with timing constraints 80 | 81 | Balances market impact costs against timing risk using quantitative optimization. 82 | 83 | ```python 84 | cerebro.addstrategy( 85 | PositionUnwindingStrategy, 86 | unwind_method="is", 87 | risk_factor=0.6, # 0=aggressive, 1=conservative 88 | volatility_threshold=0.02 89 | ) 90 | ``` 91 | 92 | **Key Features**: 93 | - Mathematically optimal execution 94 | - Adapts to market volatility 95 | - Minimizes total implementation cost 96 | - Best for sophisticated traders 97 | 98 | ### 5. Adaptive Execution 99 | **Best for**: All market conditions (recommended) 100 | 101 | Uses machine learning and real-time market analysis for dynamic optimization. 102 | 103 | ```python 104 | cerebro.addstrategy( 105 | PositionUnwindingStrategy, 106 | unwind_method="adaptive", 107 | dark_pool_preference=0.8, 108 | stealth_mode=True 109 | ) 110 | ``` 111 | 112 | **Key Features**: 113 | - Real-time market analysis 114 | - Dynamic participation rates 115 | - Multi-venue optimization 116 | - Continuous learning and adaptation 117 | 118 | ## Execution Profiles 119 | 120 | The strategy includes pre-configured execution profiles for different scenarios: 121 | 122 | ### Conservative Profile 123 | - **Use case**: Risk-averse unwinding, large positions 124 | - **Time horizon**: 8+ hours 125 | - **Participation rate**: 3-10% of volume 126 | - **Stealth mode**: Enabled 127 | - **Dark pool preference**: 90% 128 | 129 | ### Aggressive Profile 130 | - **Use case**: Fast unwinding, time-sensitive 131 | - **Time horizon**: 2 hours 132 | - **Participation rate**: 10-30% of volume 133 | - **Stealth mode**: Disabled 134 | - **Market impact**: Higher but faster completion 135 | 136 | ### Stealth Profile 137 | - **Use case**: Maximum concealment, avoid detection 138 | - **Time horizon**: 12+ hours 139 | - **Participation rate**: 2-8% of volume 140 | - **Randomization**: Maximum 141 | - **Dark pool preference**: 90% 142 | 143 | ## Risk Management Features 144 | 145 | ### Circuit Breakers 146 | The strategy automatically pauses execution when: 147 | - Volatility spikes above threshold (default 2%) 148 | - Volume drops below liquidity buffer (default 30%) 149 | - Adverse price movements occur (default 3%) 150 | 151 | ### Emergency Liquidation 152 | Automatic emergency exit when: 153 | - Position loss exceeds threshold (default 10%) 154 | - Market conditions deteriorate severely 155 | - Completion risk becomes too high 156 | 157 | ## Usage Examples 158 | 159 | ### Basic Usage 160 | ```python 161 | from algos.position_unwinding.strategy import PositionUnwindingStrategy 162 | 163 | # Add to your Backtrader cerebro 164 | cerebro.addstrategy( 165 | PositionUnwindingStrategy, 166 | unwind_method="adaptive", 167 | target_position=0, # Full unwind 168 | time_horizon=240, # 4 hours 169 | stealth_mode=True 170 | ) 171 | ``` 172 | 173 | ### Advanced Configuration 174 | ```python 175 | # Customize for your specific needs 176 | cerebro.addstrategy( 177 | PositionUnwindingStrategy, 178 | unwind_method="vwap", 179 | target_position=5000, # Partial unwind to 5000 shares 180 | max_participation_rate=0.15, 181 | min_participation_rate=0.05, 182 | volatility_threshold=0.025, 183 | liquidity_buffer=0.35, 184 | max_order_value=25000, 185 | dark_pool_preference=0.7, 186 | stealth_mode=True 187 | ) 188 | ``` 189 | 190 | ## Execution Analytics 191 | 192 | Track and analyze execution quality with comprehensive metrics: 193 | 194 | ```python 195 | from algos.position_unwinding.execution_analytics import ExecutionAnalytics 196 | 197 | analytics = ExecutionAnalytics() 198 | # ... add fills ... 199 | report = analytics.generate_execution_report("AAPL") 200 | ``` 201 | 202 | ### Available Metrics 203 | - **Implementation Shortfall**: Total execution cost breakdown 204 | - **Market Impact**: Price impact of your trades 205 | - **Timing Cost**: Opportunity cost of delayed execution 206 | - **Participation Rate**: % of market volume consumed 207 | - **Slippage**: Execution price vs. reference price 208 | - **Venue Analysis**: Performance across different venues 209 | 210 | ## Best Practices 211 | 212 | ### 1. Choose the Right Algorithm 213 | - **Stable markets**: TWAP or VWAP 214 | - **Volatile markets**: Implementation Shortfall or Adaptive 215 | - **Very large orders**: Iceberg or Stealth profile 216 | - **Time-sensitive**: Aggressive profile 217 | - **Risk-averse**: Conservative profile 218 | 219 | ### 2. Parameter Tuning 220 | - **Participation rate**: Start conservative (5-15%) 221 | - **Time horizon**: Allow adequate time (4-8 hours typical) 222 | - **Volatility threshold**: Adjust based on asset volatility 223 | - **Stealth mode**: Enable for large/sensitive positions 224 | 225 | ### 3. Market Conditions 226 | - **Avoid earnings/events**: Higher volatility and impact 227 | - **Use market hours**: Better liquidity and pricing 228 | - **Monitor news flow**: Pause during major news 229 | - **Check correlations**: Consider portfolio-level impact 230 | 231 | ### 4. Venue Selection 232 | - **Dark pools**: 40-70% for large orders 233 | - **Primary exchange**: 20-40% for price discovery 234 | - **ECNs**: 10-30% for additional liquidity 235 | - **Avoid predictable patterns**: Randomize venue selection 236 | 237 | ## Configuration 238 | 239 | Comprehensive configuration is available via `config.yaml`. Key sections: 240 | 241 | - **Strategy parameters**: Execution method, timing, participation rates 242 | - **Risk management**: Circuit breakers, position limits, emergency exits 243 | - **Venue allocation**: Dark pools, exchanges, ECN preferences 244 | - **Monitoring**: Real-time alerts, performance tracking 245 | - **Compliance**: Regulatory requirements, audit trails 246 | 247 | ## Integration 248 | 249 | The unwinding strategies integrate seamlessly with: 250 | - **Backtrader**: Direct strategy integration 251 | - **Portfolio systems**: Position and risk management 252 | - **Broker APIs**: Multi-venue execution 253 | - **Risk systems**: Real-time monitoring and controls 254 | - **Analytics**: Performance measurement and reporting 255 | 256 | ## Performance Benchmarking 257 | 258 | Benchmark your execution against: 259 | - **TWAP**: Time-weighted average price 260 | - **VWAP**: Volume-weighted average price 261 | - **Arrival price**: Price when unwinding started 262 | - **Implementation shortfall**: Industry standard metric 263 | 264 | ## Regulatory Compliance 265 | 266 | Built-in compliance features: 267 | - **Best execution**: Documentation and analysis 268 | - **MiFID II**: European execution requirements 269 | - **Reg NMS**: US order protection rules 270 | - **Audit trails**: Complete execution history 271 | - **Position limits**: Automatic enforcement 272 | 273 | ## Getting Started 274 | 275 | 1. **Install dependencies**: Ensure backtrader, numpy, pandas are available 276 | 2. **Configure strategy**: Choose execution method and parameters 277 | 3. **Set up data feeds**: Minute-level price and volume data 278 | 4. **Initialize positions**: Set starting position and target 279 | 5. **Monitor execution**: Use analytics to track performance 280 | 6. **Analyze results**: Review execution quality metrics 281 | 282 | For detailed examples, see `example_usage.py`. 283 | 284 | ## Support 285 | 286 | For questions about institutional execution strategies or implementation details, consult: 287 | - Academic papers on market microstructure 288 | - Industry best practices from major exchanges 289 | - Regulatory guidance on execution quality 290 | - Professional trading system documentation 291 | 292 | The strategies in this module implement proven institutional techniques used by major hedge funds, banks, and asset managers worldwide. -------------------------------------------------------------------------------- /scripts/ingest_binance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Stream Binance market data and save to parquet files.""" 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | import json 7 | import signal 8 | import sys 9 | from datetime import datetime, timezone 10 | from pathlib import Path 11 | from typing import Any 12 | 13 | import click 14 | import pandas as pd 15 | import websockets 16 | from websockets.exceptions import ConnectionClosed 17 | 18 | from quantdesk.utils.env import SETTINGS 19 | from quantdesk.utils.logging import get_logger 20 | 21 | log = get_logger(__name__) 22 | 23 | 24 | class BinanceDataIngester: 25 | """Real-time Binance data ingestion with parquet storage.""" 26 | 27 | def __init__(self, output_dir: str = "data/cache/binance") -> None: 28 | """Initialize the data ingester. 29 | 30 | :param output_dir: Directory to save parquet files. 31 | """ 32 | self.output_dir = Path(output_dir) 33 | self.output_dir.mkdir(parents=True, exist_ok=True) 34 | 35 | self.base_url = "wss://stream.binance.com:9443/ws" 36 | self.testnet_url = "wss://testnet.binance.vision/ws" 37 | 38 | # Use testnet if configured 39 | self.ws_url = self.testnet_url if SETTINGS.binance_key else self.base_url 40 | 41 | self.data_buffer: dict[str, list[dict[str, Any]]] = { 42 | "klines": [], 43 | "trades": [], 44 | "depth": [], 45 | "ticker": [], 46 | } 47 | self.buffer_size = 1000 48 | self.running = False 49 | self.websocket = None 50 | 51 | async def handle_kline(self, data: dict[str, Any]) -> None: 52 | """Handle kline (candlestick) data.""" 53 | kline = data["k"] 54 | record = { 55 | "timestamp": pd.to_datetime(kline["t"], unit="ms", utc=True), 56 | "symbol": kline["s"], 57 | "open": float(kline["o"]), 58 | "high": float(kline["h"]), 59 | "low": float(kline["l"]), 60 | "close": float(kline["c"]), 61 | "volume": float(kline["v"]), 62 | "quote_volume": float(kline["q"]), 63 | "trade_count": int(kline["n"]), 64 | "is_closed": kline["x"], # Whether this kline is closed 65 | } 66 | 67 | self.data_buffer["klines"].append(record) 68 | log.debug("kline.received", symbol=kline["s"], timestamp=record["timestamp"]) 69 | 70 | if len(self.data_buffer["klines"]) >= self.buffer_size: 71 | await self._flush_buffer("klines") 72 | 73 | async def handle_trade(self, data: dict[str, Any]) -> None: 74 | """Handle trade data.""" 75 | record = { 76 | "timestamp": pd.to_datetime(data["T"], unit="ms", utc=True), 77 | "symbol": data["s"], 78 | "price": float(data["p"]), 79 | "quantity": float(data["q"]), 80 | "trade_id": int(data["t"]), 81 | "buyer_maker": data["m"], # True if buyer is market maker 82 | } 83 | 84 | self.data_buffer["trades"].append(record) 85 | 86 | if len(self.data_buffer["trades"]) >= self.buffer_size: 87 | await self._flush_buffer("trades") 88 | 89 | async def handle_depth(self, data: dict[str, Any]) -> None: 90 | """Handle order book depth data.""" 91 | record = { 92 | "timestamp": pd.to_datetime(data["E"], unit="ms", utc=True), 93 | "symbol": data["s"], 94 | "first_update_id": int(data["U"]), 95 | "final_update_id": int(data["u"]), 96 | "bids": json.dumps(data["b"][:10]), # Top 10 bids 97 | "asks": json.dumps(data["a"][:10]), # Top 10 asks 98 | } 99 | 100 | self.data_buffer["depth"].append(record) 101 | 102 | if len(self.data_buffer["depth"]) >= self.buffer_size: 103 | await self._flush_buffer("depth") 104 | 105 | async def handle_ticker(self, data: dict[str, Any]) -> None: 106 | """Handle 24hr ticker statistics.""" 107 | record = { 108 | "timestamp": pd.to_datetime(data["E"], unit="ms", utc=True), 109 | "symbol": data["s"], 110 | "price_change": float(data["p"]), 111 | "price_change_percent": float(data["P"]), 112 | "weighted_avg_price": float(data["w"]), 113 | "last_price": float(data["c"]), 114 | "last_qty": float(data["Q"]), 115 | "bid_price": float(data["b"]), 116 | "bid_qty": float(data["B"]), 117 | "ask_price": float(data["a"]), 118 | "ask_qty": float(data["A"]), 119 | "open_price": float(data["o"]), 120 | "high_price": float(data["h"]), 121 | "low_price": float(data["l"]), 122 | "volume": float(data["v"]), 123 | "quote_volume": float(data["q"]), 124 | "open_time": pd.to_datetime(data["O"], unit="ms", utc=True), 125 | "close_time": pd.to_datetime(data["C"], unit="ms", utc=True), 126 | "count": int(data["n"]), 127 | } 128 | 129 | self.data_buffer["ticker"].append(record) 130 | 131 | if len(self.data_buffer["ticker"]) >= self.buffer_size: 132 | await self._flush_buffer("ticker") 133 | 134 | async def _flush_buffer(self, data_type: str) -> None: 135 | """Flush buffer to parquet file.""" 136 | if not self.data_buffer[data_type]: 137 | return 138 | 139 | df = pd.DataFrame(self.data_buffer[data_type]) 140 | 141 | # Create filename with current date 142 | date_str = datetime.now(timezone.utc).strftime("%Y%m%d") 143 | filename = self.output_dir / f"{data_type}_{date_str}.parquet" 144 | 145 | # Append to existing file or create new one 146 | if filename.exists(): 147 | existing_df = pd.read_parquet(filename) 148 | df = pd.concat([existing_df, df], ignore_index=True) 149 | 150 | df.to_parquet(filename, index=False) 151 | 152 | log.info( 153 | "data.flushed", 154 | data_type=data_type, 155 | records=len(self.data_buffer[data_type]), 156 | filename=str(filename), 157 | ) 158 | 159 | # Clear buffer 160 | self.data_buffer[data_type] = [] 161 | 162 | def create_stream_params(self, symbols: list[str]) -> list[str]: 163 | """Create WebSocket stream parameters.""" 164 | streams = [] 165 | 166 | for symbol in symbols: 167 | symbol_lower = symbol.lower() 168 | # Add different stream types 169 | streams.extend([ 170 | f"{symbol_lower}@kline_1m", # 1-minute klines 171 | f"{symbol_lower}@trade", # Individual trades 172 | f"{symbol_lower}@depth20@100ms", # Order book depth 173 | f"{symbol_lower}@ticker", # 24hr ticker 174 | ]) 175 | 176 | return streams 177 | 178 | async def start_streaming(self, symbols: list[str]) -> None: 179 | """Start streaming data for given symbols.""" 180 | streams = self.create_stream_params(symbols) 181 | stream_names = "/".join(streams) 182 | ws_url = f"{self.ws_url}/{stream_names}" 183 | 184 | log.info("ingester.starting", symbols=symbols, url=ws_url) 185 | 186 | self.running = True 187 | 188 | while self.running: 189 | try: 190 | async with websockets.connect(ws_url) as websocket: 191 | self.websocket = websocket 192 | log.info("websocket.connected") 193 | 194 | async for message in websocket: 195 | if not self.running: 196 | break 197 | 198 | try: 199 | data = json.loads(message) 200 | await self._process_message(data) 201 | except json.JSONDecodeError: 202 | log.warning("invalid.json", message=message[:100]) 203 | except Exception: 204 | log.exception("message.processing.error") 205 | 206 | except ConnectionClosed: 207 | if self.running: 208 | log.warning("websocket.disconnected.reconnecting") 209 | await asyncio.sleep(5) # Wait before reconnecting 210 | else: 211 | log.info("websocket.disconnected.shutdown") 212 | break 213 | except Exception: 214 | log.exception("websocket.error") 215 | if self.running: 216 | await asyncio.sleep(5) # Wait before reconnecting 217 | 218 | async def _process_message(self, data: dict[str, Any]) -> None: 219 | """Process incoming WebSocket message.""" 220 | if "stream" not in data: 221 | return 222 | 223 | stream = data["stream"] 224 | message_data = data["data"] 225 | 226 | if "@kline" in stream: 227 | await self.handle_kline(message_data) 228 | elif "@trade" in stream: 229 | await self.handle_trade(message_data) 230 | elif "@depth" in stream: 231 | await self.handle_depth(message_data) 232 | elif "@ticker" in stream: 233 | await self.handle_ticker(message_data) 234 | 235 | async def stop_streaming(self) -> None: 236 | """Stop streaming and flush remaining data.""" 237 | log.info("ingester.stopping") 238 | self.running = False 239 | 240 | if self.websocket: 241 | await self.websocket.close() 242 | 243 | # Flush remaining data 244 | for data_type in self.data_buffer: 245 | await self._flush_buffer(data_type) 246 | 247 | log.info("ingester.stopped") 248 | 249 | 250 | # Global ingester instance for signal handling 251 | ingester: BinanceDataIngester | None = None 252 | 253 | 254 | def signal_handler(signum: int, frame: Any) -> None: 255 | """Handle shutdown signals gracefully.""" 256 | log.info("signal.received", signal=signum) 257 | if ingester: 258 | asyncio.create_task(ingester.stop_streaming()) 259 | 260 | 261 | @click.command() 262 | @click.option( 263 | "--symbols", 264 | default="BTCUSDT,ETHUSDT,ADAUSDT,SOLUSDT,DOTUSDT", 265 | help="Comma-separated list of symbols to stream", 266 | ) 267 | @click.option( 268 | "--output-dir", 269 | default="data/cache/binance", 270 | help="Output directory for parquet files", 271 | ) 272 | def main(symbols: str, output_dir: str) -> None: 273 | """Stream Binance market data to parquet files.""" 274 | global ingester 275 | 276 | # Setup signal handlers 277 | signal.signal(signal.SIGINT, signal_handler) 278 | signal.signal(signal.SIGTERM, signal_handler) 279 | 280 | # Parse symbols 281 | symbol_list = [s.strip().upper() for s in symbols.split(",")] 282 | 283 | # Create ingester 284 | ingester = BinanceDataIngester(output_dir) 285 | 286 | # Start streaming 287 | try: 288 | asyncio.run(ingester.start_streaming(symbol_list)) 289 | except KeyboardInterrupt: 290 | log.info("ingester.interrupted") 291 | except Exception: 292 | log.exception("ingester.error") 293 | sys.exit(1) 294 | 295 | 296 | if __name__ == "__main__": 297 | main() -------------------------------------------------------------------------------- /scripts/ingest_binance.py.bak: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Stream Binance market data and save to parquet files.""" 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | import json 7 | import signal 8 | import sys 9 | from datetime import datetime, timezone 10 | from pathlib import Path 11 | from typing import Any 12 | 13 | import click 14 | import pandas as pd 15 | import websockets 16 | from websockets.exceptions import ConnectionClosed 17 | 18 | from quantdesk.utils.env import SETTINGS 19 | from quantdesk.utils.logging import get_logger 20 | 21 | log = get_logger(__name__) 22 | 23 | 24 | class BinanceDataIngester: 25 | """Real-time Binance data ingestion with parquet storage.""" 26 | 27 | def __init__(self, output_dir: str = "data/cache/binance") -> None: 28 | """Initialize the data ingester. 29 | 30 | :param output_dir: Directory to save parquet files. 31 | """ 32 | self.output_dir = Path(output_dir) 33 | self.output_dir.mkdir(parents=True, exist_ok=True) 34 | 35 | self.base_url = "wss://stream.binance.com:9443/ws" 36 | self.testnet_url = "wss://testnet.binance.vision/ws" 37 | 38 | # Use testnet if configured 39 | self.ws_url = self.testnet_url if SETTINGS.binance_key else self.base_url 40 | 41 | self.data_buffer: dict[str, list[dict[str, Any]]] = { 42 | "klines": [], 43 | "trades": [], 44 | "depth": [], 45 | "ticker": [], 46 | } 47 | self.buffer_size = 1000 48 | self.running = False 49 | self.websocket = None 50 | 51 | async def handle_kline(self, data: dict[str, Any]) -> None: 52 | """Handle kline (candlestick) data.""" 53 | kline = data["k"] 54 | record = { 55 | "timestamp": pd.to_datetime(kline["t"], unit="ms", utc=True), 56 | "symbol": kline["s"], 57 | "open": float(kline["o"]), 58 | "high": float(kline["h"]), 59 | "low": float(kline["l"]), 60 | "close": float(kline["c"]), 61 | "volume": float(kline["v"]), 62 | "quote_volume": float(kline["q"]), 63 | "trade_count": int(kline["n"]), 64 | "is_closed": kline["x"], # Whether this kline is closed 65 | } 66 | 67 | self.data_buffer["klines"].append(record) 68 | log.debug("kline.received", symbol=kline["s"], timestamp=record["timestamp"]) 69 | 70 | if len(self.data_buffer["klines"]) >= self.buffer_size: 71 | await self._flush_buffer("klines") 72 | 73 | async def handle_trade(self, data: dict[str, Any]) -> None: 74 | """Handle trade data.""" 75 | record = { 76 | "timestamp": pd.to_datetime(data["T"], unit="ms", utc=True), 77 | "symbol": data["s"], 78 | "price": float(data["p"]), 79 | "quantity": float(data["q"]), 80 | "trade_id": int(data["t"]), 81 | "buyer_maker": data["m"], # True if buyer is market maker 82 | } 83 | 84 | self.data_buffer["trades"].append(record) 85 | 86 | if len(self.data_buffer["trades"]) >= self.buffer_size: 87 | await self._flush_buffer("trades") 88 | 89 | async def handle_depth(self, data: dict[str, Any]) -> None: 90 | """Handle order book depth data.""" 91 | record = { 92 | "timestamp": pd.to_datetime(data["E"], unit="ms", utc=True), 93 | "symbol": data["s"], 94 | "first_update_id": int(data["U"]), 95 | "final_update_id": int(data["u"]), 96 | "bids": json.dumps(data["b"][:10]), # Top 10 bids 97 | "asks": json.dumps(data["a"][:10]), # Top 10 asks 98 | } 99 | 100 | self.data_buffer["depth"].append(record) 101 | 102 | if len(self.data_buffer["depth"]) >= self.buffer_size: 103 | await self._flush_buffer("depth") 104 | 105 | async def handle_ticker(self, data: dict[str, Any]) -> None: 106 | """Handle 24hr ticker statistics.""" 107 | record = { 108 | "timestamp": pd.to_datetime(data["E"], unit="ms", utc=True), 109 | "symbol": data["s"], 110 | "price_change": float(data["p"]), 111 | "price_change_percent": float(data["P"]), 112 | "weighted_avg_price": float(data["w"]), 113 | "last_price": float(data["c"]), 114 | "last_qty": float(data["Q"]), 115 | "bid_price": float(data["b"]), 116 | "bid_qty": float(data["B"]), 117 | "ask_price": float(data["a"]), 118 | "ask_qty": float(data["A"]), 119 | "open_price": float(data["o"]), 120 | "high_price": float(data["h"]), 121 | "low_price": float(data["l"]), 122 | "volume": float(data["v"]), 123 | "quote_volume": float(data["q"]), 124 | "open_time": pd.to_datetime(data["O"], unit="ms", utc=True), 125 | "close_time": pd.to_datetime(data["C"], unit="ms", utc=True), 126 | "count": int(data["n"]), 127 | } 128 | 129 | self.data_buffer["ticker"].append(record) 130 | 131 | if len(self.data_buffer["ticker"]) >= self.buffer_size: 132 | await self._flush_buffer("ticker") 133 | 134 | async def _flush_buffer(self, data_type: str) -> None: 135 | """Flush buffer to parquet file.""" 136 | if not self.data_buffer[data_type]: 137 | return 138 | 139 | df = pd.DataFrame(self.data_buffer[data_type]) 140 | 141 | # Create filename with current date 142 | date_str = datetime.now(timezone.utc).strftime("%Y%m%d") 143 | filename = self.output_dir / f"{data_type}_{date_str}.parquet" 144 | 145 | # Append to existing file or create new one 146 | if filename.exists(): 147 | existing_df = pd.read_parquet(filename) 148 | df = pd.concat([existing_df, df], ignore_index=True) 149 | 150 | df.to_parquet(filename, index=False) 151 | 152 | log.info( 153 | "data.flushed", 154 | data_type=data_type, 155 | records=len(self.data_buffer[data_type]), 156 | filename=str(filename), 157 | ) 158 | 159 | # Clear buffer 160 | self.data_buffer[data_type] = [] 161 | 162 | def create_stream_params(self, symbols: list[str]) -> list[str]: 163 | """Create WebSocket stream parameters.""" 164 | streams = [] 165 | 166 | for symbol in symbols: 167 | symbol_lower = symbol.lower() 168 | # Add different stream types 169 | streams.extend([ 170 | f"{symbol_lower}@kline_1m", # 1-minute klines 171 | f"{symbol_lower}@trade", # Individual trades 172 | f"{symbol_lower}@depth20@100ms", # Order book depth 173 | f"{symbol_lower}@ticker", # 24hr ticker 174 | ]) 175 | 176 | return streams 177 | 178 | async def start_streaming(self, symbols: list[str]) -> None: 179 | """Start streaming data for given symbols.""" 180 | streams = self.create_stream_params(symbols) 181 | stream_names = "/".join(streams) 182 | ws_url = f"{self.ws_url}/{stream_names}" 183 | 184 | log.info("ingester.starting", symbols=symbols, url=ws_url) 185 | 186 | self.running = True 187 | 188 | while self.running: 189 | try: 190 | async with websockets.connect(ws_url) as websocket: 191 | self.websocket = websocket 192 | log.info("websocket.connected") 193 | 194 | async for message in websocket: 195 | if not self.running: 196 | break 197 | 198 | try: 199 | data = json.loads(message) 200 | await self._process_message(data) 201 | except json.JSONDecodeError: 202 | log.warning("invalid.json", message=message[:100]) 203 | except Exception: 204 | log.exception("message.processing.error") 205 | 206 | except ConnectionClosed: 207 | if self.running: 208 | log.warning("websocket.disconnected.reconnecting") 209 | await asyncio.sleep(5) # Wait before reconnecting 210 | else: 211 | log.info("websocket.disconnected.shutdown") 212 | break 213 | except Exception: 214 | log.exception("websocket.error") 215 | if self.running: 216 | await asyncio.sleep(5) # Wait before reconnecting 217 | 218 | async def _process_message(self, data: dict[str, Any]) -> None: 219 | """Process incoming WebSocket message.""" 220 | if "stream" not in data: 221 | return 222 | 223 | stream = data["stream"] 224 | message_data = data["data"] 225 | 226 | if "@kline" in stream: 227 | await self.handle_kline(message_data) 228 | elif "@trade" in stream: 229 | await self.handle_trade(message_data) 230 | elif "@depth" in stream: 231 | await self.handle_depth(message_data) 232 | elif "@ticker" in stream: 233 | await self.handle_ticker(message_data) 234 | 235 | async def stop_streaming(self) -> None: 236 | """Stop streaming and flush remaining data.""" 237 | log.info("ingester.stopping") 238 | self.running = False 239 | 240 | if self.websocket: 241 | await self.websocket.close() 242 | 243 | # Flush remaining data 244 | for data_type in self.data_buffer: 245 | await self._flush_buffer(data_type) 246 | 247 | log.info("ingester.stopped") 248 | 249 | 250 | # Global ingester instance for signal handling 251 | ingester: BinanceDataIngester | None = None 252 | 253 | 254 | def signal_handler(signum: int, frame: Any) -> None: 255 | """Handle shutdown signals gracefully.""" 256 | log.info("signal.received", signal=signum) 257 | if ingester: 258 | asyncio.create_task(ingester.stop_streaming()) 259 | 260 | 261 | @click.command() 262 | @click.option( 263 | "--symbols", 264 | default="BTCUSDT,ETHUSDT,ADAUSDT,SOLUSDT,DOTUSDT", 265 | help="Comma-separated list of symbols to stream", 266 | ) 267 | @click.option( 268 | "--output-dir", 269 | default="data/cache/binance", 270 | help="Output directory for parquet files", 271 | ) 272 | def main(symbols: str, output_dir: str) -> None: 273 | """Stream Binance market data to parquet files.""" 274 | global ingester 275 | 276 | # Setup signal handlers 277 | signal.signal(signal.SIGINT, signal_handler) 278 | signal.signal(signal.SIGTERM, signal_handler) 279 | 280 | # Parse symbols 281 | symbol_list = [s.strip().upper() for s in symbols.split(",")] 282 | 283 | # Create ingester 284 | ingester = BinanceDataIngester(output_dir) 285 | 286 | # Start streaming 287 | try: 288 | asyncio.run(ingester.start_streaming(symbol_list)) 289 | except KeyboardInterrupt: 290 | log.info("ingester.interrupted") 291 | except Exception: 292 | log.exception("ingester.error") 293 | sys.exit(1) 294 | 295 | 296 | if __name__ == "__main__": 297 | main() -------------------------------------------------------------------------------- /algos/position_unwinding/execution_analytics.py: -------------------------------------------------------------------------------- 1 | """Execution analytics for position unwinding strategies.""" 2 | from __future__ import annotations 3 | 4 | from dataclasses import dataclass 5 | from datetime import datetime 6 | from typing import Dict, List, Optional 7 | 8 | import numpy as np 9 | import pandas as pd 10 | 11 | from quantdesk.utils.logging import get_logger 12 | 13 | log = get_logger(__name__) 14 | 15 | 16 | @dataclass 17 | class ExecutionFill: 18 | """Represents a single execution fill.""" 19 | timestamp: datetime 20 | symbol: str 21 | side: str # BUY or SELL 22 | quantity: int 23 | price: float 24 | venue: str = "primary" 25 | order_type: str = "market" 26 | 27 | 28 | @dataclass 29 | class ExecutionBenchmark: 30 | """Benchmark prices for execution analysis.""" 31 | arrival_price: float 32 | twap_price: float 33 | vwap_price: float 34 | close_price: float 35 | 36 | 37 | class ExecutionAnalytics: 38 | """Analytics for measuring execution quality and market impact.""" 39 | 40 | def __init__(self) -> None: 41 | self.fills: List[ExecutionFill] = [] 42 | self.benchmarks: Dict[str, ExecutionBenchmark] = {} 43 | 44 | def add_fill(self, fill: ExecutionFill) -> None: 45 | """Add an execution fill to the analytics.""" 46 | self.fills.append(fill) 47 | 48 | def set_benchmark(self, symbol: str, benchmark: ExecutionBenchmark) -> None: 49 | """Set benchmark prices for a symbol.""" 50 | self.benchmarks[symbol] = benchmark 51 | 52 | def calculate_implementation_shortfall(self, symbol: str) -> Dict[str, float]: 53 | """Calculate implementation shortfall metrics. 54 | 55 | :param symbol: Symbol to analyze. 56 | :return: Implementation shortfall breakdown in basis points. 57 | """ 58 | symbol_fills = [f for f in self.fills if f.symbol == symbol] 59 | if not symbol_fills or symbol not in self.benchmarks: 60 | return {} 61 | 62 | benchmark = self.benchmarks[symbol] 63 | arrival_price = benchmark.arrival_price 64 | 65 | # Calculate weighted average execution price 66 | total_value = sum(f.quantity * f.price for f in symbol_fills) 67 | total_quantity = sum(f.quantity for f in symbol_fills) 68 | avg_execution_price = total_value / max(total_quantity, 1) 69 | 70 | # Determine if we were buying or selling 71 | net_quantity = sum(f.quantity if f.side == "BUY" else -f.quantity for f in symbol_fills) 72 | is_buying = net_quantity > 0 73 | 74 | # Calculate implementation shortfall components 75 | market_impact = self._calculate_market_impact(symbol_fills, arrival_price, is_buying) 76 | timing_cost = self._calculate_timing_cost(symbol_fills, benchmark, is_buying) 77 | 78 | total_is = market_impact + timing_cost 79 | 80 | return { 81 | "total_implementation_shortfall": total_is, 82 | "market_impact": market_impact, 83 | "timing_cost": timing_cost, 84 | "arrival_price": arrival_price, 85 | "avg_execution_price": avg_execution_price, 86 | } 87 | 88 | def _calculate_market_impact(self, fills: List[ExecutionFill], arrival_price: float, is_buying: bool) -> float: 89 | """Calculate market impact in basis points.""" 90 | total_value = sum(f.quantity * f.price for f in fills) 91 | total_quantity = sum(f.quantity for f in fills) 92 | avg_price = total_value / max(total_quantity, 1) 93 | 94 | if is_buying: 95 | impact = (avg_price - arrival_price) / arrival_price 96 | else: 97 | impact = (arrival_price - avg_price) / arrival_price 98 | 99 | return impact * 10000 # Convert to basis points 100 | 101 | def _calculate_timing_cost(self, fills: List[ExecutionFill], benchmark: ExecutionBenchmark, is_buying: bool) -> float: 102 | """Calculate timing cost in basis points.""" 103 | arrival_price = benchmark.arrival_price 104 | close_price = benchmark.close_price 105 | 106 | if is_buying: 107 | timing_cost = (close_price - arrival_price) / arrival_price 108 | else: 109 | timing_cost = (arrival_price - close_price) / arrival_price 110 | 111 | return timing_cost * 10000 # Convert to basis points 112 | 113 | def calculate_participation_rate(self, symbol: str, market_volume: float) -> float: 114 | """Calculate participation rate as percentage of market volume. 115 | 116 | :param symbol: Symbol to analyze. 117 | :param market_volume: Total market volume during execution period. 118 | :return: Participation rate as percentage. 119 | """ 120 | symbol_fills = [f for f in self.fills if f.symbol == symbol] 121 | total_executed = sum(f.quantity for f in symbol_fills) 122 | 123 | return (total_executed / max(market_volume, 1)) * 100 124 | 125 | def calculate_venue_breakdown(self, symbol: str) -> Dict[str, float]: 126 | """Calculate execution breakdown by venue. 127 | 128 | :param symbol: Symbol to analyze. 129 | :return: Dictionary of venue -> percentage executed. 130 | """ 131 | symbol_fills = [f for f in self.fills if f.symbol == symbol] 132 | total_quantity = sum(f.quantity for f in symbol_fills) 133 | 134 | venue_quantities = {} 135 | for fill in symbol_fills: 136 | venue_quantities[fill.venue] = venue_quantities.get(fill.venue, 0) + fill.quantity 137 | 138 | return { 139 | venue: (qty / max(total_quantity, 1)) * 100 140 | for venue, qty in venue_quantities.items() 141 | } 142 | 143 | def calculate_execution_rate(self, symbol: str) -> Dict[str, float]: 144 | """Calculate execution rate statistics. 145 | 146 | :param symbol: Symbol to analyze. 147 | :return: Execution rate statistics. 148 | """ 149 | symbol_fills = [f for f in self.fills if f.symbol == symbol] 150 | if len(symbol_fills) < 2: 151 | return {} 152 | 153 | # Sort by timestamp 154 | symbol_fills.sort(key=lambda x: x.timestamp) 155 | 156 | # Calculate time intervals 157 | intervals = [] 158 | quantities = [] 159 | 160 | for i in range(1, len(symbol_fills)): 161 | time_diff = (symbol_fills[i].timestamp - symbol_fills[i-1].timestamp).total_seconds() / 60 162 | intervals.append(time_diff) 163 | quantities.append(symbol_fills[i].quantity) 164 | 165 | # Calculate rates (shares per minute) 166 | rates = [q / max(t, 1) for q, t in zip(quantities, intervals)] 167 | 168 | return { 169 | "avg_execution_rate": np.mean(rates), 170 | "median_execution_rate": np.median(rates), 171 | "std_execution_rate": np.std(rates), 172 | "min_execution_rate": np.min(rates), 173 | "max_execution_rate": np.max(rates), 174 | } 175 | 176 | def generate_execution_report(self, symbol: str) -> Dict[str, any]: 177 | """Generate comprehensive execution report. 178 | 179 | :param symbol: Symbol to analyze. 180 | :return: Comprehensive execution report. 181 | """ 182 | symbol_fills = [f for f in self.fills if f.symbol == symbol] 183 | if not symbol_fills: 184 | return {"error": "No fills found for symbol"} 185 | 186 | # Basic statistics 187 | total_quantity = sum(f.quantity for f in symbol_fills) 188 | total_value = sum(f.quantity * f.price for f in symbol_fills) 189 | avg_price = total_value / max(total_quantity, 1) 190 | 191 | # Time statistics 192 | symbol_fills.sort(key=lambda x: x.timestamp) 193 | start_time = symbol_fills[0].timestamp 194 | end_time = symbol_fills[-1].timestamp 195 | duration_minutes = (end_time - start_time).total_seconds() / 60 196 | 197 | report = { 198 | "symbol": symbol, 199 | "execution_summary": { 200 | "total_quantity": total_quantity, 201 | "total_value": total_value, 202 | "avg_execution_price": avg_price, 203 | "num_fills": len(symbol_fills), 204 | "start_time": start_time.isoformat(), 205 | "end_time": end_time.isoformat(), 206 | "duration_minutes": duration_minutes, 207 | }, 208 | "venue_breakdown": self.calculate_venue_breakdown(symbol), 209 | "execution_rates": self.calculate_execution_rate(symbol), 210 | } 211 | 212 | # Add implementation shortfall if benchmarks available 213 | if symbol in self.benchmarks: 214 | report["implementation_shortfall"] = self.calculate_implementation_shortfall(symbol) 215 | 216 | return report 217 | 218 | def export_to_dataframe(self) -> pd.DataFrame: 219 | """Export execution fills to pandas DataFrame for analysis.""" 220 | if not self.fills: 221 | return pd.DataFrame() 222 | 223 | data = [] 224 | for fill in self.fills: 225 | data.append({ 226 | "timestamp": fill.timestamp, 227 | "symbol": fill.symbol, 228 | "side": fill.side, 229 | "quantity": fill.quantity, 230 | "price": fill.price, 231 | "value": fill.quantity * fill.price, 232 | "venue": fill.venue, 233 | "order_type": fill.order_type, 234 | }) 235 | 236 | return pd.DataFrame(data) 237 | 238 | def calculate_slippage(self, symbol: str, reference_price: float) -> float: 239 | """Calculate slippage relative to reference price. 240 | 241 | :param symbol: Symbol to analyze. 242 | :param reference_price: Reference price (e.g., mid-price at order time). 243 | :return: Slippage in basis points. 244 | """ 245 | symbol_fills = [f for f in self.fills if f.symbol == symbol] 246 | if not symbol_fills: 247 | return 0.0 248 | 249 | total_value = sum(f.quantity * f.price for f in symbol_fills) 250 | total_quantity = sum(f.quantity for f in symbol_fills) 251 | avg_price = total_value / max(total_quantity, 1) 252 | 253 | # Calculate slippage (positive means worse execution) 254 | net_quantity = sum(f.quantity if f.side == "BUY" else -f.quantity for f in symbol_fills) 255 | is_buying = net_quantity > 0 256 | 257 | if is_buying: 258 | slippage = (avg_price - reference_price) / reference_price 259 | else: 260 | slippage = (reference_price - avg_price) / reference_price 261 | 262 | return slippage * 10000 # Convert to basis points 263 | 264 | 265 | class ExecutionBenchmarkCalculator: 266 | """Calculates execution benchmarks from market data.""" 267 | 268 | @staticmethod 269 | def calculate_twap(prices: List[float], start_time: datetime, end_time: datetime) -> float: 270 | """Calculate Time-Weighted Average Price. 271 | 272 | :param prices: List of prices during execution period. 273 | :param start_time: Start of execution period. 274 | :param end_time: End of execution period. 275 | :return: TWAP price. 276 | """ 277 | if not prices: 278 | return 0.0 279 | return np.mean(prices) 280 | 281 | @staticmethod 282 | def calculate_vwap(prices: List[float], volumes: List[float]) -> float: 283 | """Calculate Volume-Weighted Average Price. 284 | 285 | :param prices: List of prices. 286 | :param volumes: List of corresponding volumes. 287 | :return: VWAP price. 288 | """ 289 | if not prices or not volumes or len(prices) != len(volumes): 290 | return 0.0 291 | 292 | total_value = sum(p * v for p, v in zip(prices, volumes)) 293 | total_volume = sum(volumes) 294 | 295 | return total_value / max(total_volume, 1) 296 | 297 | @staticmethod 298 | def create_benchmark( 299 | arrival_price: float, 300 | prices: List[float], 301 | volumes: List[float], 302 | close_price: float, 303 | start_time: datetime, 304 | end_time: datetime, 305 | ) -> ExecutionBenchmark: 306 | """Create execution benchmark with all reference prices. 307 | 308 | :param arrival_price: Price when unwinding started. 309 | :param prices: Prices during execution period. 310 | :param volumes: Volumes during execution period. 311 | :param close_price: Closing price. 312 | :param start_time: Start of execution. 313 | :param end_time: End of execution. 314 | :return: ExecutionBenchmark object. 315 | """ 316 | twap = ExecutionBenchmarkCalculator.calculate_twap(prices, start_time, end_time) 317 | vwap = ExecutionBenchmarkCalculator.calculate_vwap(prices, volumes) 318 | 319 | return ExecutionBenchmark( 320 | arrival_price=arrival_price, 321 | twap_price=twap, 322 | vwap_price=vwap, 323 | close_price=close_price, 324 | ) --------------------------------------------------------------------------------