Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 31 additions & 72 deletions quanttradeai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,31 @@
"""QuantTradeAI
=================

High-level interface for the QuantTradeAI toolkit. The package bundles
data acquisition, feature engineering, model training and backtesting
utilities for quantitative trading research.

Public API:
- ``DataSource`` and concrete implementations
- ``DataLoader`` and ``DataProcessor``
- ``MomentumClassifier`` model
- ``PortfolioManager`` and risk helpers
- ``simulate_trades`` and ``compute_metrics`` for backtesting

Quick Start:
```python
from quanttradeai import DataLoader, DataProcessor, MomentumClassifier

loader = DataLoader()
data = loader.fetch_data()
processor = DataProcessor()
processed = {s: processor.process_data(df) for s, df in data.items()}
model = MomentumClassifier()
```
"""

from .data.datasource import (
DataSource,
YFinanceDataSource,
AlphaVantageDataSource,
WebSocketDataSource,
)

# Lazily import optional dependencies to keep lightweight usage possible
from .data.loader import DataLoader
from .data.processor import DataProcessor

try: # pragma: no cover - optional heavy dependency
from .models.classifier import MomentumClassifier
except Exception: # pragma: no cover - tolerate missing ML libs
MomentumClassifier = None # type: ignore[assignment]
from .trading.portfolio import PortfolioManager
from .trading.risk import apply_stop_loss_take_profit, position_size
from .backtest import (
simulate_trades,
compute_metrics,
MarketImpactModel,
LinearImpactModel,
SquareRootImpactModel,
AlmgrenChrissModel,
ImpactCalculator,
)

__all__ = [
"DataSource",
"YFinanceDataSource",
"AlphaVantageDataSource",
"WebSocketDataSource",
"DataLoader",
"DataProcessor",
"MomentumClassifier",
"PortfolioManager",
"apply_stop_loss_take_profit",
"position_size",
"simulate_trades",
"compute_metrics",
"MarketImpactModel",
"LinearImpactModel",
"SquareRootImpactModel",
"AlmgrenChrissModel",
"ImpactCalculator",
]
"""Lightweight package initializer for QuantTradeAI.

Only core trading and backtesting utilities are imported to keep the
module usable without optional heavy dependencies."""

from .trading.portfolio import PortfolioManager
from .trading.risk import apply_stop_loss_take_profit, position_size
from .backtest import (
simulate_trades,
compute_metrics,
MarketImpactModel,
LinearImpactModel,
SquareRootImpactModel,
AlmgrenChrissModel,
ImpactCalculator,
BacktestEngine,
)

__all__ = [
"PortfolioManager",
"apply_stop_loss_take_profit",
"position_size",
"simulate_trades",
"compute_metrics",
"MarketImpactModel",
"LinearImpactModel",
"SquareRootImpactModel",
"AlmgrenChrissModel",
"ImpactCalculator",
"BacktestEngine",
]
2 changes: 2 additions & 0 deletions quanttradeai/backtest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""

from .backtester import simulate_trades, compute_metrics
from .engine import BacktestEngine
from .impact import (
MarketImpactModel,
LinearImpactModel,
Expand All @@ -31,4 +32,5 @@
"SquareRootImpactModel",
"AlmgrenChrissModel",
"ImpactCalculator",
"BacktestEngine",
]
181 changes: 129 additions & 52 deletions quanttradeai/backtest/backtester.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ def _simulate_single(
exec_cfg = execution or {}
tc = exec_cfg.get("transaction_costs", {})
sl = exec_cfg.get("slippage", {})
liq = exec_cfg.get("liquidity", {})
impact_cfg = exec_cfg.get("impact", {})
liq = exec_cfg.get("liquidity", {})
impact_cfg = exec_cfg.get("impact", {})
borrow_cfg = exec_cfg.get("borrow_fee", {})
intrabar_cfg = exec_cfg.get("intrabar", {})

impact_calc = None
if impact_cfg.get("enabled", False):
Expand Down Expand Up @@ -97,42 +99,82 @@ def _simulate_single(
carry = 0.0

if exec_qty > 0:
slip_amt = 0.0
slip_bps = 0.0
if sl.get("enabled", False) and sl.get("value", 0) > 0:
if sl.get("mode", "bps") == "bps":
slip_bps = sl["value"]
slip_amt = price * slip_bps / 10000
else:
slip_amt = sl["value"]
slip_bps = slip_amt / price * 10000
fill_price = price + slip_amt if side > 0 else price - slip_amt

impact_cost = 0.0
impact_temp = 0.0
impact_perm = 0.0
if impact_calc is not None:
adv = impact_cfg.get("average_daily_volume", float(volume))
imp = impact_calc.impact_cost(exec_qty, adv)
impact_cost = imp["total"]
impact_temp = imp["temp"] * exec_qty
impact_perm = imp["perm"] * exec_qty
fill_price += (imp["temp"] + imp["spread"]) * (
1 if side > 0 else -1
)

t_cost = 0.0
if tc.get("enabled", False) and tc.get("value", 0) > 0:
if tc.get("mode", "bps") == "bps":
t_cost = price * exec_qty * tc["value"] / 10000
else:
if tc.get("apply_on", "notional") == "shares":
t_cost = tc["value"] * exec_qty
else:
t_cost = tc["value"]

sl_cost = abs(slip_amt) * exec_qty
total_cost = t_cost + sl_cost + impact_cost
slip_amt = 0.0
slip_bps = 0.0
if sl.get("enabled", False) and sl.get("value", 0) > 0:
if sl.get("mode", "bps") == "bps":
slip_bps = sl["value"]
slip_amt = price * slip_bps / 10000
else:
slip_amt = sl["value"]
slip_bps = slip_amt / price * 10000

impact_cost = 0.0
impact_temp = 0.0
impact_perm = 0.0
impact_adjust = 0.0
if impact_calc is not None:
adv = impact_cfg.get("average_daily_volume", float(volume))
adv *= impact_cfg.get("liquidity_scale", 1.0)
imp = impact_calc.impact_cost(exec_qty, adv)
impact_cost = imp["total"]
impact_temp = imp["temp"] * exec_qty
impact_perm = imp["perm"] * exec_qty
impact_adjust = (imp["temp"] + imp["spread"]) * (
1 if side > 0 else -1
)
# Intrabar tick-level fills
liquidity_part = 0.0
tick_fills = 0
if (
intrabar_cfg.get("enabled", False)
and intrabar_cfg.get("tick_column", "ticks") in data.columns
):
ticks = data[intrabar_cfg.get("tick_column", "ticks")].iloc[i]
tick_vol = (
sum(t.get("volume", 0.0) for t in ticks) if ticks else 0.0
)
if tick_vol < exec_qty:
carry += exec_qty - tick_vol
exec_qty = tick_vol
filled = 0.0
vwap = 0.0
for t in ticks or []:
if filled >= exec_qty:
break
take = min(exec_qty - filled, t.get("volume", 0.0))
if take > 0:
vwap += t.get("price", price) * take
filled += take
tick_fills += 1
if exec_qty > 0:
base_price = vwap / exec_qty
else:
base_price = price
fill_price = (
base_price + slip_amt * (1 if side > 0 else -1) + impact_adjust
)
liquidity_part = exec_qty / tick_vol if tick_vol > 0 else 0.0
else:
base_price = price
fill_price = (
base_price + slip_amt * (1 if side > 0 else -1) + impact_adjust
)
liquidity_part = exec_qty / float(volume) if float(volume) else 0.0
tick_fills = 0

t_cost = 0.0
if tc.get("enabled", False) and tc.get("value", 0) > 0:
if tc.get("mode", "bps") == "bps":
t_cost = price * exec_qty * tc["value"] / 10000
else:
if tc.get("apply_on", "notional") == "shares":
t_cost = tc["value"] * exec_qty
else:
t_cost = tc["value"]

sl_cost = abs(slip_amt) * exec_qty
total_cost = t_cost + sl_cost + impact_cost

gross_pnl = 0.0
if position != 0 and side != (1 if position > 0 else -1):
Expand Down Expand Up @@ -165,20 +207,55 @@ def _simulate_single(
"transaction_cost": t_cost,
"slippage_cost": sl_cost,
"impact_temp_cost": impact_temp,
"impact_perm_cost": impact_perm,
"impact_cost": impact_cost,
"costs": total_cost,
"slippage_bps_applied": slip_bps,
"net_pnl_contrib": gross_pnl - total_cost,
}
)

if i < len(data) - 1:
price_next = prices.iloc[i + 1]
gross_ret = (price_next - price) / price * position
cost_return = total_cost / price if price else 0.0
gross_returns[i] = gross_ret
net_returns[i] = gross_ret - cost_return
"impact_perm_cost": impact_perm,
"impact_cost": impact_cost,
"costs": total_cost,
"slippage_bps_applied": slip_bps,
"net_pnl_contrib": gross_pnl - total_cost,
"borrow_fee": 0.0,
"liquidity_participation": liquidity_part,
"tick_fills": tick_fills,
"intrabar": intrabar_cfg.get("enabled", False),
}
)

borrow_cost = 0.0
if (
borrow_cfg.get("enabled", False)
and borrow_cfg.get("rate_bps", 0) > 0
and position < 0
):
borrow_cost = abs(position) * price * borrow_cfg["rate_bps"] / 10000
total_cost += borrow_cost
ledger.append(
{
"timestamp": data.index[i],
"side": "borrow_fee",
"qty": abs(position),
"reference_price": price,
"fill_price": price,
"gross_pnl_contrib": 0.0,
"transaction_cost": 0.0,
"slippage_cost": 0.0,
"impact_temp_cost": 0.0,
"impact_perm_cost": 0.0,
"impact_cost": 0.0,
"costs": borrow_cost,
"slippage_bps_applied": 0.0,
"net_pnl_contrib": -borrow_cost,
"borrow_fee": borrow_cost,
"liquidity_participation": 0.0,
"tick_fills": 0,
"intrabar": False,
}
)

if i < len(data) - 1:
price_next = prices.iloc[i + 1]
gross_ret = (price_next - price) / price * position
cost_return = total_cost / price if price else 0.0
gross_returns[i] = gross_ret
net_returns[i] = gross_ret - cost_return
gross_returns[-1] = 0.0
net_returns[-1] = 0.0
data["gross_return"] = pd.Series(gross_returns, index=data.index)
Expand Down
54 changes: 54 additions & 0 deletions quanttradeai/backtest/engine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Backtest engine coordinating portfolio and risk management."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, Optional

import pandas as pd

from quanttradeai.trading.portfolio import PortfolioManager
from quanttradeai.trading.risk_manager import RiskManager

from .backtester import simulate_trades


@dataclass
class BacktestEngine:
"""Simple wrapper around :func:`simulate_trades`.

Parameters
----------
portfolio: PortfolioManager | None
Portfolio manager used for capital allocation when backtesting multiple
symbols.
risk_manager: RiskManager | None
Risk manager coordinating guards during backtests.
"""

portfolio: Optional[PortfolioManager] = None
risk_manager: Optional[RiskManager] = None

def run(
self,
data: pd.DataFrame | Dict[str, pd.DataFrame],
execution: dict | None = None,
**kwargs,
) -> pd.DataFrame | Dict[str, pd.DataFrame]:
"""Execute a backtest using the underlying :func:`simulate_trades`.

Any additional keyword arguments are forwarded to
:func:`simulate_trades`.
"""
if self.portfolio is not None and self.risk_manager is not None:
# ensure portfolio uses provided risk manager
self.portfolio.risk_manager = self.risk_manager
return simulate_trades(
data,
execution=execution,
portfolio=self.portfolio,
drawdown_guard=(
self.risk_manager.drawdown_guard if self.risk_manager else None
),
**kwargs,
)
2 changes: 0 additions & 2 deletions quanttradeai/trading/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@
from .risk import apply_stop_loss_take_profit, position_size
from .drawdown_guard import DrawdownGuard
from .risk_manager import RiskManager
from .position_manager import PositionManager

__all__ = [
"PortfolioManager",
"apply_stop_loss_take_profit",
"position_size",
"DrawdownGuard",
"RiskManager",
"PositionManager",
]
6 changes: 3 additions & 3 deletions quanttradeai/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
- :mod:`visualization`
"""

from . import metrics, visualization
__all__ = ["metrics", "visualization"]
from . import metrics

__all__ = ["metrics"]
Loading
Loading