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.
- oq_backtest-0.1.0/.gitignore +82 -0
- oq_backtest-0.1.0/PKG-INFO +49 -0
- oq_backtest-0.1.0/README.md +24 -0
- oq_backtest-0.1.0/pyproject.toml +37 -0
- oq_backtest-0.1.0/src/oq_backtest/__init__.py +91 -0
- oq_backtest-0.1.0/src/oq_backtest/costs.py +277 -0
- oq_backtest-0.1.0/src/oq_backtest/engine.py +187 -0
- oq_backtest-0.1.0/src/oq_backtest/intraday.py +167 -0
- oq_backtest-0.1.0/src/oq_backtest/metrics.py +147 -0
- oq_backtest-0.1.0/src/oq_backtest/result.py +105 -0
- oq_backtest-0.1.0/src/oq_backtest/slippage.py +186 -0
- oq_backtest-0.1.0/src/oq_backtest/strategies.py +139 -0
- oq_backtest-0.1.0/src/oq_backtest/tax.py +125 -0
- oq_backtest-0.1.0/src/oq_backtest/walkforward.py +96 -0
- oq_backtest-0.1.0/tests/test_costs.py +124 -0
- oq_backtest-0.1.0/tests/test_engine.py +155 -0
- oq_backtest-0.1.0/tests/test_intraday.py +117 -0
- oq_backtest-0.1.0/tests/test_metrics.py +102 -0
- oq_backtest-0.1.0/tests/test_slippage.py +116 -0
- oq_backtest-0.1.0/tests/test_strategies.py +85 -0
- oq_backtest-0.1.0/tests/test_tax.py +66 -0
- oq_backtest-0.1.0/tests/test_walkforward.py +64 -0
|
@@ -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
|
+
]
|