oq-backtest 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ wheels/
12
+ develop-eggs/
13
+ eggs/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ *.manifest
18
+ *.spec
19
+ pip-log.txt
20
+ pip-delete-this-directory.txt
21
+
22
+ # uv
23
+ .venv/
24
+ venv/
25
+ env/
26
+ ENV/
27
+ .python-version
28
+
29
+ # Testing / coverage
30
+ .pytest_cache/
31
+ .coverage
32
+ .coverage.*
33
+ htmlcov/
34
+ .tox/
35
+ .nox/
36
+ coverage.xml
37
+ *.cover
38
+ .cache
39
+
40
+ # mypy / ruff
41
+ .mypy_cache/
42
+ .ruff_cache/
43
+ .dmypy.json
44
+ dmypy.json
45
+
46
+ # Jupyter
47
+ .ipynb_checkpoints/
48
+ *.ipynb_checkpoints
49
+
50
+ # Data / artifacts
51
+ data/
52
+ *.parquet
53
+ *.duckdb
54
+ *.duckdb.wal
55
+ *.csv.gz
56
+ *.zip
57
+ .openquant/
58
+
59
+ !packages/*/tests/fixtures/**
60
+
61
+ # IDE / OS
62
+ .idea/
63
+ .vscode/
64
+ *.swp
65
+ *.swo
66
+ .DS_Store
67
+ Thumbs.db
68
+
69
+ # Logs
70
+ *.log
71
+ logs/
72
+
73
+ # Secrets
74
+ .env
75
+ .env.*
76
+ !.env.example
77
+ *.pem
78
+ *.key
79
+
80
+ # build artifacts
81
+ dist/
82
+ *.egg-info/
@@ -0,0 +1,49 @@
1
+ Metadata-Version: 2.4
2
+ Name: oq-backtest
3
+ Version: 0.1.0
4
+ Summary: Honest, vectorized backtester for Indian equities with full STT/brokerage/GST/slippage/tax cost modeling.
5
+ Project-URL: Homepage, https://github.com/revorhq/openquant
6
+ Project-URL: Repository, https://github.com/revorhq/openquant
7
+ Project-URL: Issues, https://github.com/revorhq/openquant/issues
8
+ Author: OpenQuant India Contributors
9
+ License: Apache-2.0
10
+ Keywords: backtesting,finance,india,nse,quant,trading
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Office/Business :: Financial :: Investment
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: numpy>=1.24
22
+ Requires-Dist: oq-core
23
+ Requires-Dist: pandas>=2.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # oq-backtest
27
+
28
+ Honest, vectorized backtester for Indian equities.
29
+
30
+ Models every cost an Indian retail trader actually pays — STT, brokerage,
31
+ exchange charges, GST, stamp duty, SEBI fees, slippage, and STCG/LTCG —
32
+ with broker presets for Zerodha, Upstox, Fyers, Dhan. Outputs gross vs net
33
+ equity curves side-by-side with a full cost attribution breakdown.
34
+
35
+ ```bash
36
+ pip install oq-backtest
37
+ ```
38
+
39
+ ```python
40
+ import oq_backtest as ob
41
+ result = ob.backtest(signals, prices, costs="zerodha")
42
+ print(result.tearsheet())
43
+ ```
44
+
45
+ Includes walk-forward / out-of-sample utilities and an intraday layer for
46
+ 1–60 min bars with session square-off.
47
+
48
+ Part of [OpenQuant India](https://github.com/revorhq/openquant) — honest, open
49
+ source quant infrastructure for Indian markets. Apache 2.0.
@@ -0,0 +1,24 @@
1
+ # oq-backtest
2
+
3
+ Honest, vectorized backtester for Indian equities.
4
+
5
+ Models every cost an Indian retail trader actually pays — STT, brokerage,
6
+ exchange charges, GST, stamp duty, SEBI fees, slippage, and STCG/LTCG —
7
+ with broker presets for Zerodha, Upstox, Fyers, Dhan. Outputs gross vs net
8
+ equity curves side-by-side with a full cost attribution breakdown.
9
+
10
+ ```bash
11
+ pip install oq-backtest
12
+ ```
13
+
14
+ ```python
15
+ import oq_backtest as ob
16
+ result = ob.backtest(signals, prices, costs="zerodha")
17
+ print(result.tearsheet())
18
+ ```
19
+
20
+ Includes walk-forward / out-of-sample utilities and an intraday layer for
21
+ 1–60 min bars with session square-off.
22
+
23
+ Part of [OpenQuant India](https://github.com/revorhq/openquant) — honest, open
24
+ source quant infrastructure for Indian markets. Apache 2.0.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "oq-backtest"
3
+ version = "0.1.0"
4
+ description = "Honest, vectorized backtester for Indian equities with full STT/brokerage/GST/slippage/tax cost modeling."
5
+ requires-python = ">=3.11"
6
+ license = { text = "Apache-2.0" }
7
+ readme = "README.md"
8
+ authors = [{ name = "OpenQuant India Contributors" }]
9
+ keywords = ["quant", "trading", "india", "nse", "backtesting", "finance"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "Intended Audience :: Financial and Insurance Industry",
14
+ "License :: OSI Approved :: Apache Software License",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Office/Business :: Financial :: Investment",
20
+ ]
21
+ dependencies = [
22
+ "numpy>=1.24",
23
+ "pandas>=2.0",
24
+ "oq-core",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/revorhq/openquant"
29
+ Repository = "https://github.com/revorhq/openquant"
30
+ Issues = "https://github.com/revorhq/openquant/issues"
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/oq_backtest"]
@@ -0,0 +1,91 @@
1
+ """``oq-backtest``: honest, vectorized backtester for Indian equities."""
2
+
3
+ from oq_backtest.costs import (
4
+ DHAN_DELIVERY,
5
+ DHAN_INTRADAY,
6
+ FULL_SERVICE_DELIVERY,
7
+ FYERS_DELIVERY,
8
+ FYERS_INTRADAY,
9
+ PRESETS,
10
+ UPSTOX_DELIVERY,
11
+ UPSTOX_INTRADAY,
12
+ ZERODHA_DELIVERY,
13
+ ZERODHA_INTRADAY,
14
+ CostBreakdown,
15
+ CostConfig,
16
+ TaxConfig,
17
+ compute_costs,
18
+ resolve_config,
19
+ )
20
+ from oq_backtest.engine import backtest
21
+ from oq_backtest.intraday import (
22
+ NSE_CLOSE,
23
+ NSE_OPEN,
24
+ IntradayConfig,
25
+ apply_square_off,
26
+ backtest_intraday,
27
+ intraday_summary,
28
+ is_intraday_preset,
29
+ )
30
+ from oq_backtest.result import BacktestResult
31
+ from oq_backtest.slippage import (
32
+ FixedBpsSlippage,
33
+ SlippageModel,
34
+ SpreadSlippage,
35
+ VolumeParticipationSlippage,
36
+ resolve_slippage,
37
+ )
38
+ from oq_backtest.strategies import (
39
+ equal_weight,
40
+ mean_reversion_signal,
41
+ momentum_signal,
42
+ rebalance_dates,
43
+ synthetic_universe,
44
+ )
45
+ from oq_backtest.tax import TaxBreakdown, estimate_taxes
46
+ from oq_backtest.walkforward import Fold, train_test_split, walk_forward
47
+
48
+ __version__ = "0.1.0"
49
+
50
+ __all__ = [
51
+ "DHAN_DELIVERY",
52
+ "DHAN_INTRADAY",
53
+ "FULL_SERVICE_DELIVERY",
54
+ "FYERS_DELIVERY",
55
+ "FYERS_INTRADAY",
56
+ "NSE_CLOSE",
57
+ "NSE_OPEN",
58
+ "PRESETS",
59
+ "UPSTOX_DELIVERY",
60
+ "UPSTOX_INTRADAY",
61
+ "ZERODHA_DELIVERY",
62
+ "ZERODHA_INTRADAY",
63
+ "BacktestResult",
64
+ "CostBreakdown",
65
+ "CostConfig",
66
+ "FixedBpsSlippage",
67
+ "Fold",
68
+ "IntradayConfig",
69
+ "SlippageModel",
70
+ "SpreadSlippage",
71
+ "TaxBreakdown",
72
+ "TaxConfig",
73
+ "VolumeParticipationSlippage",
74
+ "__version__",
75
+ "apply_square_off",
76
+ "backtest",
77
+ "backtest_intraday",
78
+ "compute_costs",
79
+ "equal_weight",
80
+ "estimate_taxes",
81
+ "intraday_summary",
82
+ "is_intraday_preset",
83
+ "mean_reversion_signal",
84
+ "momentum_signal",
85
+ "rebalance_dates",
86
+ "resolve_config",
87
+ "resolve_slippage",
88
+ "synthetic_universe",
89
+ "train_test_split",
90
+ "walk_forward",
91
+ ]
@@ -0,0 +1,277 @@
1
+ """Indian equity cost engine.
2
+
3
+ Implements the regulatory and broker charges that a real Indian equity trade
4
+ incurs, and exposes a small set of broker presets so a user can swap a single
5
+ string in :func:`oq_backtest.backtest` and get a realistic net P&L.
6
+
7
+ All rates are expressed as decimal fractions of notional (``0.001`` == 0.1%).
8
+ Rates are calibrated against published charge sheets as of 2024-2025; consumers
9
+ should treat them as a baseline and override via :class:`CostConfig` for exact
10
+ broker / state / segment specifics.
11
+
12
+ References
13
+ ----------
14
+ * SEBI charges: Rs. 10 per crore of turnover (1e-6 of notional).
15
+ * NSE cash exchange transaction charge (revised Oct 2024): 0.00297%.
16
+ * GST: 18% on (brokerage + exchange + SEBI) charges only.
17
+ * STT delivery: 0.1% on both buy and sell legs.
18
+ * STT intraday: 0.025% on sell leg only.
19
+ * Stamp duty (delivery, buy only): 0.015%. Intraday buy: 0.003%.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from collections.abc import Mapping
25
+ from dataclasses import dataclass
26
+ from typing import Literal
27
+
28
+ import numpy as np
29
+
30
+ Side = Literal["buy", "sell"]
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class CostConfig:
35
+ """Per-leg cost configuration for an Indian equity broker.
36
+
37
+ All ``*_rate`` fields are decimal fractions of order notional. Brokerage
38
+ additionally supports a per-order ``min`` floor and ``max`` ceiling in
39
+ INR (matching the "0.03% or Rs. 20, whichever lower" convention).
40
+ """
41
+
42
+ brokerage_rate: float = 0.0
43
+ brokerage_min: float = 0.0
44
+ brokerage_max: float = float("inf")
45
+ stt_buy_rate: float = 0.001
46
+ stt_sell_rate: float = 0.001
47
+ exchange_rate: float = 0.0000297
48
+ sebi_rate: float = 1e-6
49
+ stamp_duty_buy_rate: float = 0.00015
50
+ gst_rate: float = 0.18
51
+ is_intraday: bool = False
52
+
53
+ def __post_init__(self) -> None:
54
+ for name in (
55
+ "brokerage_rate",
56
+ "stt_buy_rate",
57
+ "stt_sell_rate",
58
+ "exchange_rate",
59
+ "sebi_rate",
60
+ "stamp_duty_buy_rate",
61
+ "gst_rate",
62
+ ):
63
+ value = getattr(self, name)
64
+ if value < 0:
65
+ raise ValueError(f"{name} must be >= 0, got {value}")
66
+ if self.brokerage_min < 0:
67
+ raise ValueError("brokerage_min must be >= 0")
68
+ if self.brokerage_max < self.brokerage_min:
69
+ raise ValueError("brokerage_max must be >= brokerage_min")
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class CostBreakdown:
74
+ """Per-rebalance cost decomposition in INR."""
75
+
76
+ brokerage: float = 0.0
77
+ stt: float = 0.0
78
+ exchange: float = 0.0
79
+ sebi: float = 0.0
80
+ gst: float = 0.0
81
+ stamp_duty: float = 0.0
82
+
83
+ @property
84
+ def total(self) -> float:
85
+ return self.brokerage + self.stt + self.exchange + self.sebi + self.gst + self.stamp_duty
86
+
87
+ def as_dict(self) -> dict[str, float]:
88
+ return {
89
+ "brokerage": self.brokerage,
90
+ "stt": self.stt,
91
+ "exchange": self.exchange,
92
+ "sebi": self.sebi,
93
+ "gst": self.gst,
94
+ "stamp_duty": self.stamp_duty,
95
+ "total": self.total,
96
+ }
97
+
98
+ def __add__(self, other: CostBreakdown) -> CostBreakdown:
99
+ if not isinstance(other, CostBreakdown):
100
+ return NotImplemented
101
+ return CostBreakdown(
102
+ brokerage=self.brokerage + other.brokerage,
103
+ stt=self.stt + other.stt,
104
+ exchange=self.exchange + other.exchange,
105
+ sebi=self.sebi + other.sebi,
106
+ gst=self.gst + other.gst,
107
+ stamp_duty=self.stamp_duty + other.stamp_duty,
108
+ )
109
+
110
+
111
+ ZERODHA_DELIVERY = CostConfig()
112
+
113
+ ZERODHA_INTRADAY = CostConfig(
114
+ brokerage_rate=0.0003,
115
+ brokerage_max=20.0,
116
+ stt_buy_rate=0.0,
117
+ stt_sell_rate=0.00025,
118
+ stamp_duty_buy_rate=0.00003,
119
+ is_intraday=True,
120
+ )
121
+
122
+ UPSTOX_DELIVERY = CostConfig()
123
+
124
+ UPSTOX_INTRADAY = CostConfig(
125
+ brokerage_rate=0.0005,
126
+ brokerage_max=20.0,
127
+ stt_buy_rate=0.0,
128
+ stt_sell_rate=0.00025,
129
+ stamp_duty_buy_rate=0.00003,
130
+ is_intraday=True,
131
+ )
132
+
133
+ FYERS_DELIVERY = CostConfig()
134
+
135
+ FYERS_INTRADAY = CostConfig(
136
+ brokerage_rate=0.0003,
137
+ brokerage_max=20.0,
138
+ stt_buy_rate=0.0,
139
+ stt_sell_rate=0.00025,
140
+ stamp_duty_buy_rate=0.00003,
141
+ is_intraday=True,
142
+ )
143
+
144
+ DHAN_DELIVERY = CostConfig()
145
+
146
+ DHAN_INTRADAY = CostConfig(
147
+ brokerage_rate=0.0003,
148
+ brokerage_max=20.0,
149
+ stt_buy_rate=0.0,
150
+ stt_sell_rate=0.00025,
151
+ stamp_duty_buy_rate=0.00003,
152
+ is_intraday=True,
153
+ )
154
+
155
+ FULL_SERVICE_DELIVERY = CostConfig(
156
+ brokerage_rate=0.005,
157
+ brokerage_min=0.0,
158
+ brokerage_max=float("inf"),
159
+ )
160
+
161
+
162
+ PRESETS: Mapping[str, CostConfig] = {
163
+ "zerodha": ZERODHA_DELIVERY,
164
+ "zerodha_intraday": ZERODHA_INTRADAY,
165
+ "upstox": UPSTOX_DELIVERY,
166
+ "upstox_intraday": UPSTOX_INTRADAY,
167
+ "fyers": FYERS_DELIVERY,
168
+ "fyers_intraday": FYERS_INTRADAY,
169
+ "dhan": DHAN_DELIVERY,
170
+ "dhan_intraday": DHAN_INTRADAY,
171
+ "full_service": FULL_SERVICE_DELIVERY,
172
+ "zero": CostConfig(
173
+ brokerage_rate=0.0,
174
+ stt_buy_rate=0.0,
175
+ stt_sell_rate=0.0,
176
+ exchange_rate=0.0,
177
+ sebi_rate=0.0,
178
+ stamp_duty_buy_rate=0.0,
179
+ gst_rate=0.0,
180
+ ),
181
+ }
182
+
183
+
184
+ def resolve_config(costs: str | CostConfig) -> CostConfig:
185
+ """Look up a preset by name, or return a :class:`CostConfig` unchanged."""
186
+ if isinstance(costs, CostConfig):
187
+ return costs
188
+ if isinstance(costs, str):
189
+ key = costs.lower()
190
+ if key not in PRESETS:
191
+ raise KeyError(f"unknown cost preset {costs!r}; known presets: {sorted(PRESETS)}")
192
+ return PRESETS[key]
193
+ raise TypeError(f"costs must be str or CostConfig, got {type(costs).__name__}")
194
+
195
+
196
+ def _brokerage_per_order(notionals: np.ndarray, cfg: CostConfig) -> np.ndarray:
197
+ """Apply per-order min/max brokerage to an array of per-symbol notionals."""
198
+ raw = notionals * cfg.brokerage_rate
199
+ if cfg.brokerage_min > 0:
200
+ raw = np.where(notionals > 0, np.maximum(raw, cfg.brokerage_min), raw)
201
+ if np.isfinite(cfg.brokerage_max):
202
+ raw = np.where(notionals > 0, np.minimum(raw, cfg.brokerage_max), raw)
203
+ return raw
204
+
205
+
206
+ def compute_costs(
207
+ buy_notionals: np.ndarray | float,
208
+ sell_notionals: np.ndarray | float,
209
+ cfg: CostConfig,
210
+ ) -> CostBreakdown:
211
+ """Compute the full Indian-market cost breakdown for one rebalance.
212
+
213
+ Parameters
214
+ ----------
215
+ buy_notionals, sell_notionals:
216
+ Per-order absolute notional values in INR. May be scalars or arrays.
217
+ Each element is treated as an independent order for brokerage min/max.
218
+ cfg:
219
+ Cost configuration; build one yourself or use :data:`PRESETS`.
220
+ """
221
+ buys = np.atleast_1d(np.asarray(buy_notionals, dtype=float))
222
+ sells = np.atleast_1d(np.asarray(sell_notionals, dtype=float))
223
+
224
+ buy_total = float(buys.sum())
225
+ sell_total = float(sells.sum())
226
+
227
+ brokerage_buy = float(_brokerage_per_order(buys, cfg).sum())
228
+ brokerage_sell = float(_brokerage_per_order(sells, cfg).sum())
229
+ brokerage = brokerage_buy + brokerage_sell
230
+
231
+ stt = buy_total * cfg.stt_buy_rate + sell_total * cfg.stt_sell_rate
232
+ exchange = (buy_total + sell_total) * cfg.exchange_rate
233
+ sebi = (buy_total + sell_total) * cfg.sebi_rate
234
+ gst = (brokerage + exchange + sebi) * cfg.gst_rate
235
+ stamp = buy_total * cfg.stamp_duty_buy_rate
236
+
237
+ return CostBreakdown(
238
+ brokerage=brokerage,
239
+ stt=stt,
240
+ exchange=exchange,
241
+ sebi=sebi,
242
+ gst=gst,
243
+ stamp_duty=stamp,
244
+ )
245
+
246
+
247
+ @dataclass(frozen=True, slots=True)
248
+ class TaxConfig:
249
+ """Indian equity capital gains tax estimator.
250
+
251
+ Not investment advice. Holding period thresholds and rates are based on
252
+ rules as of FY2024-25. Override when legislation changes.
253
+ """
254
+
255
+ short_term_days: int = 365
256
+ stcg_rate: float = 0.15
257
+ ltcg_rate: float = 0.125
258
+ ltcg_exempt_inr: float = 125_000.0
259
+
260
+
261
+ __all__ = [
262
+ "DHAN_DELIVERY",
263
+ "DHAN_INTRADAY",
264
+ "FULL_SERVICE_DELIVERY",
265
+ "FYERS_DELIVERY",
266
+ "FYERS_INTRADAY",
267
+ "PRESETS",
268
+ "UPSTOX_DELIVERY",
269
+ "UPSTOX_INTRADAY",
270
+ "ZERODHA_DELIVERY",
271
+ "ZERODHA_INTRADAY",
272
+ "CostBreakdown",
273
+ "CostConfig",
274
+ "TaxConfig",
275
+ "compute_costs",
276
+ "resolve_config",
277
+ ]