tradingapi 0.3.12__tar.gz → 0.4.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.
- {tradingapi-0.3.12 → tradingapi-0.4.0}/PKG-INFO +12 -2
- {tradingapi-0.3.12 → tradingapi-0.4.0}/README.md +11 -1
- {tradingapi-0.3.12 → tradingapi-0.4.0}/pyproject.toml +1 -1
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_calculate_delta_realtime_quotes.py +3 -1
- tradingapi-0.4.0/tests/test_market_hours_config.py +140 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/attribution.py +28 -11
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/broker_base.py +2 -2
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/config/config_sample.yaml +22 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/config.py +153 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/dhan.py +24 -10
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/fivepaisa.py +57 -15
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/flattrade.py +26 -10
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/ib.py +1 -1
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/icicidirect.py +14 -2
- tradingapi-0.4.0/tradingapi/market_hours.py +25 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/shoonya.py +27 -11
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/utils.py +97 -32
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/PKG-INFO +12 -2
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/SOURCES.txt +2 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/setup.cfg +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_broker_side_terminal_order.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_find_option_with_delta.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_fivepaisa_historical_recovery.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_fivepaisa_stream_reconnect.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/__init__.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/allocation.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/config/commissions_20241216.yaml +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/error_handling.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/exceptions.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/globals.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/icicidirect_generate_session.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/market_data_exchanges.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/proxy_utils.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/span.py +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/dependency_links.txt +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/entry_points.txt +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/requires.txt +0 -0
- {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tradingapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Trade integration with brokers
|
|
5
5
|
Author-email: Pankaj Sharma <sharma.pankaj.kumar@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -212,6 +212,16 @@ commissions:
|
|
|
212
212
|
tz: "Asia/Kolkata"
|
|
213
213
|
datapath: "/path/to/data"
|
|
214
214
|
market_open_time: "09:15:00"
|
|
215
|
+
market_close_time: "15:30:00" # Fallback when no schedule applies
|
|
216
|
+
market_hours:
|
|
217
|
+
- effective_date: "2026-08-03"
|
|
218
|
+
exchanges:
|
|
219
|
+
NSE:
|
|
220
|
+
CASH: {open_time: "09:15:00", close_time: "15:35:00"}
|
|
221
|
+
FNO: {open_time: "09:15:00", close_time: "15:40:00"}
|
|
222
|
+
BSE:
|
|
223
|
+
CASH: {open_time: "09:15:00", close_time: "15:35:00"}
|
|
224
|
+
FNO: {open_time: "09:15:00", close_time: "15:40:00"}
|
|
215
225
|
|
|
216
226
|
# FivePaisa Broker Configuration
|
|
217
227
|
FIVEPAISA:
|
|
@@ -568,7 +578,7 @@ historical_data = broker.get_historical(
|
|
|
568
578
|
date_end="2024-12-16",
|
|
569
579
|
exchange="N",
|
|
570
580
|
periodicity="1m", # 1m, 5m, 1d, etc.
|
|
571
|
-
market_close_time="15:
|
|
581
|
+
# market_close_time="15:40:00" # Optional per-call override
|
|
572
582
|
)
|
|
573
583
|
|
|
574
584
|
# Access data
|
|
@@ -193,6 +193,16 @@ commissions:
|
|
|
193
193
|
tz: "Asia/Kolkata"
|
|
194
194
|
datapath: "/path/to/data"
|
|
195
195
|
market_open_time: "09:15:00"
|
|
196
|
+
market_close_time: "15:30:00" # Fallback when no schedule applies
|
|
197
|
+
market_hours:
|
|
198
|
+
- effective_date: "2026-08-03"
|
|
199
|
+
exchanges:
|
|
200
|
+
NSE:
|
|
201
|
+
CASH: {open_time: "09:15:00", close_time: "15:35:00"}
|
|
202
|
+
FNO: {open_time: "09:15:00", close_time: "15:40:00"}
|
|
203
|
+
BSE:
|
|
204
|
+
CASH: {open_time: "09:15:00", close_time: "15:35:00"}
|
|
205
|
+
FNO: {open_time: "09:15:00", close_time: "15:40:00"}
|
|
196
206
|
|
|
197
207
|
# FivePaisa Broker Configuration
|
|
198
208
|
FIVEPAISA:
|
|
@@ -549,7 +559,7 @@ historical_data = broker.get_historical(
|
|
|
549
559
|
date_end="2024-12-16",
|
|
550
560
|
exchange="N",
|
|
551
561
|
periodicity="1m", # 1m, 5m, 1d, etc.
|
|
552
|
-
market_close_time="15:
|
|
562
|
+
# market_close_time="15:40:00" # Optional per-call override
|
|
553
563
|
)
|
|
554
564
|
|
|
555
565
|
# Access data
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import datetime as dt
|
|
1
2
|
import math
|
|
2
3
|
import sys
|
|
3
4
|
from types import SimpleNamespace
|
|
@@ -12,11 +13,12 @@ from tradingapi import utils
|
|
|
12
13
|
class CalculateDeltaRealtimeQuotesTest(unittest.TestCase):
|
|
13
14
|
def test_calculate_delta_returns_nan_without_live_bid_ask(self):
|
|
14
15
|
ticker = SimpleNamespace(bid=0.0, ask=0.0, prior_close=123.45)
|
|
16
|
+
expiry = (dt.date.today() + dt.timedelta(days=30)).strftime("%Y%m%d")
|
|
15
17
|
|
|
16
18
|
with patch.object(utils, "get_price", return_value=ticker):
|
|
17
19
|
delta = utils.calculate_delta(
|
|
18
20
|
brokers=[],
|
|
19
|
-
long_symbol="
|
|
21
|
+
long_symbol=f"TCS_OPT_{expiry}_PUT_2160",
|
|
20
22
|
price_f=2325.1,
|
|
21
23
|
exchange="NFO",
|
|
22
24
|
mds="mds",
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, "/home/psharma/onedrive/code/tradingapi2")
|
|
7
|
+
|
|
8
|
+
from tradingapi import config
|
|
9
|
+
from tradingapi import market_hours
|
|
10
|
+
from tradingapi import utils
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
MARKET_CONFIG = {
|
|
14
|
+
"market_open_time": "09:15:00",
|
|
15
|
+
"market_close_time": "15:30:00",
|
|
16
|
+
"market_hours": [
|
|
17
|
+
{
|
|
18
|
+
"effective_date": "2026-08-03",
|
|
19
|
+
"exchanges": {
|
|
20
|
+
"NSE": {
|
|
21
|
+
"CASH": {"open_time": "09:15:00", "close_time": "15:35:00"},
|
|
22
|
+
"FNO": {"open_time": "09:15:00", "close_time": "15:40:00"},
|
|
23
|
+
},
|
|
24
|
+
"BSE": {
|
|
25
|
+
"CASH": {"open_time": "09:15:00", "close_time": "15:35:00"},
|
|
26
|
+
"FNO": {"open_time": "09:15:00", "close_time": "15:40:00"},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MarketHoursConfigTest(unittest.TestCase):
|
|
35
|
+
def setUp(self):
|
|
36
|
+
self.config = config.get_config()
|
|
37
|
+
self.config_patch = patch.object(self.config, "configs", MARKET_CONFIG)
|
|
38
|
+
self.config_patch.start()
|
|
39
|
+
|
|
40
|
+
def tearDown(self):
|
|
41
|
+
self.config_patch.stop()
|
|
42
|
+
|
|
43
|
+
def test_pre_effective_date_uses_legacy_fallback(self):
|
|
44
|
+
self.assertEqual(
|
|
45
|
+
config.get_market_close_time(
|
|
46
|
+
"NFO",
|
|
47
|
+
symbol="NIFTY_OPT_20260730_CALL_25000",
|
|
48
|
+
as_of="2026-07-30",
|
|
49
|
+
),
|
|
50
|
+
"15:30:00",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def test_nse_cash_and_fno_are_resolved_separately(self):
|
|
54
|
+
self.assertEqual(
|
|
55
|
+
config.get_market_close_time("NSE", symbol="RELIANCE_STK___", as_of="2026-08-03"),
|
|
56
|
+
"15:35:00",
|
|
57
|
+
)
|
|
58
|
+
self.assertEqual(
|
|
59
|
+
config.get_market_close_time("NSE", symbol="NIFTY_FUT_20260827__", as_of="2026-08-03"),
|
|
60
|
+
"15:40:00",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def test_exchange_alias_infers_fno(self):
|
|
64
|
+
self.assertEqual(
|
|
65
|
+
config.get_market_close_time("BFO", as_of="2026-08-03"),
|
|
66
|
+
"15:40:00",
|
|
67
|
+
)
|
|
68
|
+
self.assertEqual(
|
|
69
|
+
config.get_market_close_time(
|
|
70
|
+
symbol="SENSEX_OPT_20260827_CALL_80000", as_of="2026-08-03"
|
|
71
|
+
),
|
|
72
|
+
"15:40:00",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def test_session_bounds_uses_symbol_segment(self):
|
|
76
|
+
session_date = dt.date(2026, 8, 3)
|
|
77
|
+
cash = market_hours.session_bounds(session_date, exchange="NSE", symbol="NIFTY_IND___")
|
|
78
|
+
fno = market_hours.session_bounds(
|
|
79
|
+
session_date, exchange="NSE", symbol="NIFTY_OPT_20260827_CALL_25000"
|
|
80
|
+
)
|
|
81
|
+
self.assertEqual(cash[1].time(), dt.time(15, 35))
|
|
82
|
+
self.assertEqual(fno[1].time(), dt.time(15, 40))
|
|
83
|
+
|
|
84
|
+
def test_latest_close_covers_mixed_market_services(self):
|
|
85
|
+
self.assertEqual(
|
|
86
|
+
market_hours.latest_close_datetime(dt.date(2026, 8, 3)).time(),
|
|
87
|
+
dt.time(15, 40),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def test_latest_effective_schedule_wins(self):
|
|
91
|
+
updated = {
|
|
92
|
+
**MARKET_CONFIG,
|
|
93
|
+
"market_hours": MARKET_CONFIG["market_hours"]
|
|
94
|
+
+ [
|
|
95
|
+
{
|
|
96
|
+
"effective_date": "2027-01-01",
|
|
97
|
+
"exchanges": {
|
|
98
|
+
"NSE": {
|
|
99
|
+
"FNO": {
|
|
100
|
+
"open_time": "09:20:00",
|
|
101
|
+
"close_time": "15:45:00",
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
],
|
|
107
|
+
}
|
|
108
|
+
with patch.object(self.config, "configs", updated):
|
|
109
|
+
self.assertEqual(
|
|
110
|
+
config.get_market_close_time("NFO", as_of="2027-01-02"),
|
|
111
|
+
"15:45:00",
|
|
112
|
+
)
|
|
113
|
+
self.assertEqual(
|
|
114
|
+
config.get_market_open_time("NFO", as_of="2027-01-02"),
|
|
115
|
+
"09:20:00",
|
|
116
|
+
)
|
|
117
|
+
self.assertFalse(
|
|
118
|
+
config.is_within_market_hours(
|
|
119
|
+
dt.datetime(2027, 1, 2, 9, 17), exchange="NFO"
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
self.assertTrue(
|
|
123
|
+
config.is_within_market_hours(
|
|
124
|
+
dt.datetime(2027, 1, 2, 9, 21), exchange="NFO"
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def test_derivative_expiry_uses_schedule_for_expiry_date(self):
|
|
129
|
+
before = utils._derivative_expiry_datetime(
|
|
130
|
+
"20260730", "NIFTY_OPT_20260730_CALL_25000", "NFO"
|
|
131
|
+
)
|
|
132
|
+
after = utils._derivative_expiry_datetime(
|
|
133
|
+
"20260827", "NIFTY_OPT_20260827_CALL_25000", "NFO"
|
|
134
|
+
)
|
|
135
|
+
self.assertEqual(before.strftime("%H:%M:%S"), "15:30:00")
|
|
136
|
+
self.assertEqual(after.strftime("%H:%M:%S"), "15:40:00")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
unittest.main()
|
|
@@ -25,6 +25,7 @@ import pandas as pd
|
|
|
25
25
|
|
|
26
26
|
from . import trading_logger
|
|
27
27
|
from .broker_base import BrokerBase
|
|
28
|
+
from .config import get_market_close_time
|
|
28
29
|
from .utils import (
|
|
29
30
|
get_price_at_time,
|
|
30
31
|
get_future_underlying_price,
|
|
@@ -54,6 +55,17 @@ _HISTORICAL_BROKER_REDIS_DB = 0
|
|
|
54
55
|
_iv_cache: Dict[Tuple[str, str], float] = {} # (symbol, time_key) -> iv
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
def _market_close_datetime(date_str: str, symbol: str, exchange: str) -> dt.datetime:
|
|
59
|
+
close_time = get_market_close_time(
|
|
60
|
+
exchange=exchange, symbol=symbol, as_of=date_str
|
|
61
|
+
)
|
|
62
|
+
return dt.datetime.strptime(f"{date_str} {close_time}", "%Y%m%d %H:%M:%S")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _market_eod_datetime(date_str: str, symbol: str, exchange: str) -> dt.datetime:
|
|
66
|
+
return _market_close_datetime(date_str, symbol, exchange) - dt.timedelta(minutes=1)
|
|
67
|
+
|
|
68
|
+
|
|
57
69
|
def _is_missing_number(value: Any) -> bool:
|
|
58
70
|
return value is None or (isinstance(value, float) and value != value)
|
|
59
71
|
|
|
@@ -141,7 +153,7 @@ def get_historical_close_price(
|
|
|
141
153
|
return None
|
|
142
154
|
|
|
143
155
|
hist_broker = _get_historical_broker(broker)
|
|
144
|
-
eod_dt =
|
|
156
|
+
eod_dt = _market_eod_datetime(date_key, symbol, exchange)
|
|
145
157
|
result = get_price_at_time(
|
|
146
158
|
cast(BrokerBase, hist_broker), symbol, exchange, as_of=eod_dt, mds="mds", refresh_mapping=False
|
|
147
159
|
)
|
|
@@ -321,7 +333,7 @@ def get_iv_for_symbol(
|
|
|
321
333
|
try:
|
|
322
334
|
if "_OPT_" in symbol:
|
|
323
335
|
expiry_str = symbol.split("_")[2]
|
|
324
|
-
expiry_dt =
|
|
336
|
+
expiry_dt = _market_close_datetime(expiry_str, symbol, exchange)
|
|
325
337
|
if time >= expiry_dt:
|
|
326
338
|
return 0.0
|
|
327
339
|
except Exception:
|
|
@@ -813,8 +825,14 @@ def calculate_attribution_for_trade(
|
|
|
813
825
|
)
|
|
814
826
|
if len(prior_biz_date_str) == 10 and prior_biz_date_str[4] == "-":
|
|
815
827
|
prior_biz_date_str = prior_biz_date_str.replace("-", "")
|
|
816
|
-
|
|
817
|
-
|
|
828
|
+
entry_exchange = "BSE" if "SENSEX" in combo_symbol else "NSE"
|
|
829
|
+
entry_dt = _market_eod_datetime(
|
|
830
|
+
prior_biz_date_str, combo_symbol, entry_exchange
|
|
831
|
+
)
|
|
832
|
+
attribution_exchange = "BSE" if "SENSEX" in combo_symbol else "NSE"
|
|
833
|
+
attribution_day_market_close = _market_close_datetime(
|
|
834
|
+
attribution_date_str, combo_symbol, attribution_exchange
|
|
835
|
+
)
|
|
818
836
|
current_naive = get_naive_dt(current_time)
|
|
819
837
|
current_dt = (
|
|
820
838
|
current_naive.to_pydatetime()
|
|
@@ -1204,7 +1222,7 @@ def mtm_entry_price(
|
|
|
1204
1222
|
|
|
1205
1223
|
try:
|
|
1206
1224
|
exchange = "BSE" if "SENSEX" in symbol else "NSE"
|
|
1207
|
-
prior_eod_dt =
|
|
1225
|
+
prior_eod_dt = _market_eod_datetime(prior_date_str, symbol, exchange)
|
|
1208
1226
|
|
|
1209
1227
|
if "?" in symbol:
|
|
1210
1228
|
legs = parse_combo_symbol(symbol)
|
|
@@ -1273,7 +1291,7 @@ def mtm_exit_price(
|
|
|
1273
1291
|
except ValueError:
|
|
1274
1292
|
mtm_date_obj = None
|
|
1275
1293
|
|
|
1276
|
-
|
|
1294
|
+
mtm_close_dt = _market_close_datetime(mtm_date_str, symbol, exchange)
|
|
1277
1295
|
today = dt.date.today()
|
|
1278
1296
|
|
|
1279
1297
|
if broker is None:
|
|
@@ -1286,8 +1304,7 @@ def mtm_exit_price(
|
|
|
1286
1304
|
else:
|
|
1287
1305
|
try:
|
|
1288
1306
|
exit_time_dt = parse_datetime(exit_time_str)
|
|
1289
|
-
|
|
1290
|
-
use_mtm_exit = exit_time_dt > mtm_date_dt
|
|
1307
|
+
use_mtm_exit = exit_time_dt > mtm_close_dt
|
|
1291
1308
|
except (ValueError, TypeError):
|
|
1292
1309
|
use_mtm_exit = False
|
|
1293
1310
|
|
|
@@ -1318,7 +1335,7 @@ def mtm_exit_price(
|
|
|
1318
1335
|
if use_mtm_exit:
|
|
1319
1336
|
if mtm_date_obj and mtm_date_obj == today:
|
|
1320
1337
|
current_time = dt.datetime.now().time()
|
|
1321
|
-
market_close_time =
|
|
1338
|
+
market_close_time = mtm_close_dt.time()
|
|
1322
1339
|
if current_time < market_close_time:
|
|
1323
1340
|
try:
|
|
1324
1341
|
price = _combo_or_single_price(None, last=True)
|
|
@@ -1330,7 +1347,7 @@ def mtm_exit_price(
|
|
|
1330
1347
|
)
|
|
1331
1348
|
|
|
1332
1349
|
try:
|
|
1333
|
-
mtm_eod_dt =
|
|
1350
|
+
mtm_eod_dt = _market_eod_datetime(mtm_date_str, symbol, exchange)
|
|
1334
1351
|
mtm_price = _combo_or_single_price(mtm_eod_dt, last=False)
|
|
1335
1352
|
if mtm_price is not None:
|
|
1336
1353
|
return float(mtm_price)
|
|
@@ -1340,7 +1357,7 @@ def mtm_exit_price(
|
|
|
1340
1357
|
return entry_price
|
|
1341
1358
|
else:
|
|
1342
1359
|
try:
|
|
1343
|
-
mtm_eod_dt =
|
|
1360
|
+
mtm_eod_dt = _market_eod_datetime(mtm_date_str, symbol, exchange)
|
|
1344
1361
|
mtm_price = _combo_or_single_price(mtm_eod_dt, last=False)
|
|
1345
1362
|
if mtm_price is not None:
|
|
1346
1363
|
return float(mtm_price)
|
|
@@ -1024,7 +1024,7 @@ class BrokerBase(ABC):
|
|
|
1024
1024
|
date_end: Union[str, dt.datetime, dt.date] = get_tradingapi_now().strftime("%Y-%m-%d"),
|
|
1025
1025
|
exchange: str = "N",
|
|
1026
1026
|
periodicity: str = "1m",
|
|
1027
|
-
market_close_time: str =
|
|
1027
|
+
market_close_time: Optional[str] = None,
|
|
1028
1028
|
refresh_mapping: bool = False,
|
|
1029
1029
|
) -> Dict[str, List[HistoricalData]]:
|
|
1030
1030
|
"""
|
|
@@ -1036,7 +1036,7 @@ class BrokerBase(ABC):
|
|
|
1036
1036
|
date_end: End date for historical data (can be string, datetime, or date object)
|
|
1037
1037
|
exchange: Exchange name
|
|
1038
1038
|
periodicity: Data periodicity
|
|
1039
|
-
market_close_time:
|
|
1039
|
+
market_close_time: Optional override; configuration is used when omitted
|
|
1040
1040
|
refresh_mapping: If True, load symbol mapping from date_end's symbols CSV instead of cached mapping.
|
|
1041
1041
|
|
|
1042
1042
|
Returns:
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
# bhavcopy path - this is the path where symbol files from brokers are saved
|
|
2
2
|
bhavcopy_folder : /home/psharma/onedrive/rfiles/data/bhavcopy
|
|
3
3
|
|
|
4
|
+
tz: "Asia/Kolkata"
|
|
5
|
+
datapath: "/path/to/data"
|
|
6
|
+
market_open_time: "09:15:00"
|
|
7
|
+
market_close_time: "15:30:00" # Backward-compatible fallback
|
|
8
|
+
market_hours:
|
|
9
|
+
- effective_date: "2026-08-03"
|
|
10
|
+
exchanges:
|
|
11
|
+
NSE:
|
|
12
|
+
CASH:
|
|
13
|
+
open_time: "09:15:00"
|
|
14
|
+
close_time: "15:35:00"
|
|
15
|
+
FNO:
|
|
16
|
+
open_time: "09:15:00"
|
|
17
|
+
close_time: "15:40:00"
|
|
18
|
+
BSE:
|
|
19
|
+
CASH:
|
|
20
|
+
open_time: "09:15:00"
|
|
21
|
+
close_time: "15:35:00"
|
|
22
|
+
FNO:
|
|
23
|
+
open_time: "09:15:00"
|
|
24
|
+
close_time: "15:40:00"
|
|
25
|
+
|
|
4
26
|
# Optional: proxy for broker HTTP requests (e.g. connect, save_symbol_data).
|
|
5
27
|
# USE_PROXY: trading API calls. USE_PROXY_SYMBOL_DOWNLOAD: symbol master download only.
|
|
6
28
|
proxy:
|
|
@@ -541,6 +541,159 @@ def get_config() -> Config:
|
|
|
541
541
|
return cast(Config, _config_instance)
|
|
542
542
|
|
|
543
543
|
|
|
544
|
+
_EXCHANGE_ALIASES = {
|
|
545
|
+
"N": "NSE",
|
|
546
|
+
"NSE": "NSE",
|
|
547
|
+
"NFO": "NSE",
|
|
548
|
+
"B": "BSE",
|
|
549
|
+
"BSE": "BSE",
|
|
550
|
+
"BFO": "BSE",
|
|
551
|
+
}
|
|
552
|
+
_FNO_EXCHANGES = {"NFO", "BFO"}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _market_hours_date(value: Any) -> dt.date:
|
|
556
|
+
if value is None:
|
|
557
|
+
return dt.date.today()
|
|
558
|
+
if isinstance(value, dt.datetime):
|
|
559
|
+
return value.date()
|
|
560
|
+
if isinstance(value, dt.date):
|
|
561
|
+
return value
|
|
562
|
+
text = str(value).strip()[:10]
|
|
563
|
+
for fmt in ("%Y-%m-%d", "%Y%m%d"):
|
|
564
|
+
try:
|
|
565
|
+
return dt.datetime.strptime(text, fmt).date()
|
|
566
|
+
except ValueError:
|
|
567
|
+
continue
|
|
568
|
+
raise ValueError(f"Invalid market-hours date: {value}")
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _market_time_or_default(value: Any, default: str) -> str:
|
|
572
|
+
candidate = str(value or default)
|
|
573
|
+
try:
|
|
574
|
+
dt.datetime.strptime(candidate, "%H:%M:%S")
|
|
575
|
+
return candidate
|
|
576
|
+
except ValueError:
|
|
577
|
+
return default
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _get_market_session_time(
|
|
581
|
+
time_key: str,
|
|
582
|
+
fallback_key: str,
|
|
583
|
+
default: str,
|
|
584
|
+
exchange: Optional[str] = None,
|
|
585
|
+
market: Optional[str] = None,
|
|
586
|
+
symbol: Optional[str] = None,
|
|
587
|
+
as_of: Any = None,
|
|
588
|
+
) -> str:
|
|
589
|
+
cfg = get_config()
|
|
590
|
+
fallback = _market_time_or_default(cfg.get(fallback_key, default), default)
|
|
591
|
+
symbol_key = str(symbol or "").upper()
|
|
592
|
+
exchange_key = str(exchange or "").strip().upper()
|
|
593
|
+
if not exchange_key:
|
|
594
|
+
exchange_key = "BSE" if "SENSEX" in symbol_key else "NSE"
|
|
595
|
+
normalized_exchange = _EXCHANGE_ALIASES.get(exchange_key, exchange_key)
|
|
596
|
+
if market is None:
|
|
597
|
+
normalized_market = (
|
|
598
|
+
"FNO"
|
|
599
|
+
if exchange_key in _FNO_EXCHANGES or "_FUT_" in symbol_key or "_OPT_" in symbol_key
|
|
600
|
+
else "CASH"
|
|
601
|
+
)
|
|
602
|
+
else:
|
|
603
|
+
market_key = str(market).strip().upper()
|
|
604
|
+
normalized_market = (
|
|
605
|
+
"FNO"
|
|
606
|
+
if market_key in {"FNO", "FO", "DERIVATIVES", "DERIVATIVE"}
|
|
607
|
+
else market_key
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
try:
|
|
611
|
+
target_date = _market_hours_date(as_of)
|
|
612
|
+
except ValueError:
|
|
613
|
+
return fallback
|
|
614
|
+
|
|
615
|
+
schedules = cfg.get("market_hours", []) or []
|
|
616
|
+
if isinstance(schedules, dict):
|
|
617
|
+
schedules = schedules.get("schedules", []) or []
|
|
618
|
+
applicable = []
|
|
619
|
+
for schedule in schedules if isinstance(schedules, list) else []:
|
|
620
|
+
if not isinstance(schedule, dict) or not schedule.get("effective_date"):
|
|
621
|
+
continue
|
|
622
|
+
try:
|
|
623
|
+
effective_date = _market_hours_date(schedule["effective_date"])
|
|
624
|
+
except ValueError:
|
|
625
|
+
continue
|
|
626
|
+
if effective_date <= target_date:
|
|
627
|
+
applicable.append((effective_date, schedule))
|
|
628
|
+
|
|
629
|
+
for _, schedule in sorted(applicable, key=lambda item: item[0], reverse=True):
|
|
630
|
+
exchanges = schedule.get("exchanges") or {}
|
|
631
|
+
exchange_hours = exchanges.get(normalized_exchange) or exchanges.get("DEFAULT") or {}
|
|
632
|
+
market_hours = exchange_hours.get(normalized_market) or exchange_hours.get("DEFAULT") or {}
|
|
633
|
+
session_time = market_hours.get(time_key) if isinstance(market_hours, dict) else None
|
|
634
|
+
if session_time:
|
|
635
|
+
return _market_time_or_default(session_time, fallback)
|
|
636
|
+
return fallback
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def get_market_open_time(
|
|
640
|
+
exchange: Optional[str] = None,
|
|
641
|
+
market: Optional[str] = None,
|
|
642
|
+
symbol: Optional[str] = None,
|
|
643
|
+
as_of: Any = None,
|
|
644
|
+
default: str = "09:15:00",
|
|
645
|
+
) -> str:
|
|
646
|
+
"""Return the effective open time for an exchange and market."""
|
|
647
|
+
return _get_market_session_time(
|
|
648
|
+
"open_time",
|
|
649
|
+
"market_open_time",
|
|
650
|
+
default,
|
|
651
|
+
exchange=exchange,
|
|
652
|
+
market=market,
|
|
653
|
+
symbol=symbol,
|
|
654
|
+
as_of=as_of,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def get_market_close_time(
|
|
659
|
+
exchange: Optional[str] = None,
|
|
660
|
+
market: Optional[str] = None,
|
|
661
|
+
symbol: Optional[str] = None,
|
|
662
|
+
as_of: Any = None,
|
|
663
|
+
default: str = "15:30:00",
|
|
664
|
+
) -> str:
|
|
665
|
+
"""Return the effective close time for an exchange and market."""
|
|
666
|
+
return _get_market_session_time(
|
|
667
|
+
"close_time",
|
|
668
|
+
"market_close_time",
|
|
669
|
+
default,
|
|
670
|
+
exchange=exchange,
|
|
671
|
+
market=market,
|
|
672
|
+
symbol=symbol,
|
|
673
|
+
as_of=as_of,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def is_within_market_hours(
|
|
678
|
+
timestamp: dt.datetime,
|
|
679
|
+
exchange: Optional[str] = None,
|
|
680
|
+
market: Optional[str] = None,
|
|
681
|
+
symbol: Optional[str] = None,
|
|
682
|
+
market_open_time: Optional[str] = None,
|
|
683
|
+
market_close_time: Optional[str] = None,
|
|
684
|
+
) -> bool:
|
|
685
|
+
"""Return whether a timestamp is inside its effective trading session."""
|
|
686
|
+
open_time = market_open_time or get_market_open_time(
|
|
687
|
+
exchange=exchange, market=market, symbol=symbol, as_of=timestamp
|
|
688
|
+
)
|
|
689
|
+
close_time = market_close_time or get_market_close_time(
|
|
690
|
+
exchange=exchange, market=market, symbol=symbol, as_of=timestamp
|
|
691
|
+
)
|
|
692
|
+
open_t = dt.datetime.strptime(open_time, "%H:%M:%S").time()
|
|
693
|
+
close_t = dt.datetime.strptime(close_time, "%H:%M:%S").time()
|
|
694
|
+
return open_t <= timestamp.time() < close_t
|
|
695
|
+
|
|
696
|
+
|
|
544
697
|
def get_fno_freeze_limit(broker_name: str, underlying: str) -> Optional[int]:
|
|
545
698
|
"""Return max contracts per FNO order for (broker, underlying).
|
|
546
699
|
|
|
@@ -34,7 +34,12 @@ from .broker_base import (
|
|
|
34
34
|
Price,
|
|
35
35
|
_normalize_as_of_date,
|
|
36
36
|
)
|
|
37
|
-
from .config import
|
|
37
|
+
from .config import (
|
|
38
|
+
get_config,
|
|
39
|
+
get_market_close_time,
|
|
40
|
+
get_market_open_time,
|
|
41
|
+
is_within_market_hours,
|
|
42
|
+
)
|
|
38
43
|
from chameli.dateutils import parse_datetime
|
|
39
44
|
from .utils import (
|
|
40
45
|
delete_broker_order_id,
|
|
@@ -1672,7 +1677,7 @@ class Dhan(BrokerBase):
|
|
|
1672
1677
|
date_end=lambda x: _validate_datetime_input(x),
|
|
1673
1678
|
exchange=lambda x: isinstance(x, str) and len(x.strip()) > 0,
|
|
1674
1679
|
periodicity=lambda x: isinstance(x, str) and len(x.strip()) > 0,
|
|
1675
|
-
market_close_time=lambda x: isinstance(x, str) and len(x.strip()) > 0,
|
|
1680
|
+
market_close_time=lambda x: x is None or (isinstance(x, str) and len(x.strip()) > 0),
|
|
1676
1681
|
)
|
|
1677
1682
|
@retry_on_error(max_retries=2, delay=1.0, backoff_factor=2.0)
|
|
1678
1683
|
def get_historical(
|
|
@@ -1682,8 +1687,8 @@ class Dhan(BrokerBase):
|
|
|
1682
1687
|
date_end: Union[str, dt.datetime, dt.date] = get_tradingapi_now().strftime("%Y-%m-%d"),
|
|
1683
1688
|
exchange: str = "N",
|
|
1684
1689
|
periodicity: str = "1m",
|
|
1685
|
-
market_open_time: str =
|
|
1686
|
-
market_close_time: str =
|
|
1690
|
+
market_open_time: Optional[str] = None,
|
|
1691
|
+
market_close_time: Optional[str] = None,
|
|
1687
1692
|
refresh_mapping: bool = False,
|
|
1688
1693
|
) -> Dict[str, List[HistoricalData]]:
|
|
1689
1694
|
"""
|
|
@@ -1749,6 +1754,12 @@ class Dhan(BrokerBase):
|
|
|
1749
1754
|
out[long_symbol] = []
|
|
1750
1755
|
continue
|
|
1751
1756
|
security_id = int(security_id)
|
|
1757
|
+
resolved_market_close_time = market_close_time or get_market_close_time(
|
|
1758
|
+
exchange=exchange, symbol=long_symbol, as_of=date_end
|
|
1759
|
+
)
|
|
1760
|
+
resolved_market_open_time = market_open_time or get_market_open_time(
|
|
1761
|
+
exchange=exchange, symbol=long_symbol, as_of=date_end
|
|
1762
|
+
)
|
|
1752
1763
|
|
|
1753
1764
|
# Determine instrument_type from exchange_segment
|
|
1754
1765
|
_INDEX_UNDERLYINGS = ("NIFTY", "BANKNIFTY", "FINNIFTY", "MIDCPNIFTY", "SENSEX")
|
|
@@ -1800,9 +1811,6 @@ class Dhan(BrokerBase):
|
|
|
1800
1811
|
open_interest = raw.get("open_interest", [])
|
|
1801
1812
|
|
|
1802
1813
|
tz_ist = pytz.timezone("Asia/Kolkata")
|
|
1803
|
-
market_open = pd.to_datetime(market_open_time).time()
|
|
1804
|
-
market_close = pd.to_datetime(market_close_time).time()
|
|
1805
|
-
|
|
1806
1814
|
for i in range(len(timestamps)):
|
|
1807
1815
|
try:
|
|
1808
1816
|
epoch = int(timestamps[i])
|
|
@@ -1811,7 +1819,13 @@ class Dhan(BrokerBase):
|
|
|
1811
1819
|
)
|
|
1812
1820
|
|
|
1813
1821
|
if use_intraday:
|
|
1814
|
-
if not (
|
|
1822
|
+
if not is_within_market_hours(
|
|
1823
|
+
ts,
|
|
1824
|
+
exchange=exchange,
|
|
1825
|
+
symbol=long_symbol,
|
|
1826
|
+
market_open_time=market_open_time,
|
|
1827
|
+
market_close_time=market_close_time,
|
|
1828
|
+
):
|
|
1815
1829
|
continue
|
|
1816
1830
|
else:
|
|
1817
1831
|
ts = ts.floor("D")
|
|
@@ -1853,8 +1867,8 @@ class Dhan(BrokerBase):
|
|
|
1853
1867
|
# Always attempt when end date is today; replace any existing bar for that calendar day.
|
|
1854
1868
|
tz_ist = pytz.timezone("Asia/Kolkata")
|
|
1855
1869
|
target_d = date_end_dt.date()
|
|
1856
|
-
market_open_t = pd.to_datetime(
|
|
1857
|
-
market_close_t = pd.to_datetime(
|
|
1870
|
+
market_open_t = pd.to_datetime(resolved_market_open_time).time()
|
|
1871
|
+
market_close_t = pd.to_datetime(resolved_market_close_time).time()
|
|
1858
1872
|
|
|
1859
1873
|
intraday_response = self._fetch_dhan_historical(
|
|
1860
1874
|
security_id=security_id,
|