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.
Files changed (38) hide show
  1. {tradingapi-0.3.12 → tradingapi-0.4.0}/PKG-INFO +12 -2
  2. {tradingapi-0.3.12 → tradingapi-0.4.0}/README.md +11 -1
  3. {tradingapi-0.3.12 → tradingapi-0.4.0}/pyproject.toml +1 -1
  4. {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_calculate_delta_realtime_quotes.py +3 -1
  5. tradingapi-0.4.0/tests/test_market_hours_config.py +140 -0
  6. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/attribution.py +28 -11
  7. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/broker_base.py +2 -2
  8. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/config/config_sample.yaml +22 -0
  9. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/config.py +153 -0
  10. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/dhan.py +24 -10
  11. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/fivepaisa.py +57 -15
  12. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/flattrade.py +26 -10
  13. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/ib.py +1 -1
  14. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/icicidirect.py +14 -2
  15. tradingapi-0.4.0/tradingapi/market_hours.py +25 -0
  16. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/shoonya.py +27 -11
  17. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/utils.py +97 -32
  18. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/PKG-INFO +12 -2
  19. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/SOURCES.txt +2 -0
  20. {tradingapi-0.3.12 → tradingapi-0.4.0}/setup.cfg +0 -0
  21. {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_broker_side_terminal_order.py +0 -0
  22. {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_find_option_with_delta.py +0 -0
  23. {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_fivepaisa_historical_recovery.py +0 -0
  24. {tradingapi-0.3.12 → tradingapi-0.4.0}/tests/test_fivepaisa_stream_reconnect.py +0 -0
  25. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/__init__.py +0 -0
  26. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/allocation.py +0 -0
  27. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/config/commissions_20241216.yaml +0 -0
  28. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/error_handling.py +0 -0
  29. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/exceptions.py +0 -0
  30. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/globals.py +0 -0
  31. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/icicidirect_generate_session.py +0 -0
  32. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/market_data_exchanges.py +0 -0
  33. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/proxy_utils.py +0 -0
  34. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi/span.py +0 -0
  35. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/dependency_links.txt +0 -0
  36. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/entry_points.txt +0 -0
  37. {tradingapi-0.3.12 → tradingapi-0.4.0}/tradingapi.egg-info/requires.txt +0 -0
  38. {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.12
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:30:00"
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:30:00"
562
+ # market_close_time="15:40:00" # Optional per-call override
553
563
  )
554
564
 
555
565
  # Access data
@@ -28,7 +28,7 @@ packages = ["tradingapi"]
28
28
 
29
29
  [project]
30
30
  name = "tradingapi"
31
- version = "0.3.12"
31
+ version = "0.4.0"
32
32
  description = "Trade integration with brokers"
33
33
  readme = "README.md"
34
34
  license = "MIT"
@@ -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="TCS_OPT_20260630_PUT_2160",
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 = dt.datetime.strptime(date_key + " 15:29:00", "%Y%m%d %H:%M:%S")
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 = dt.datetime.strptime(expiry_str + " 15:30:00", "%Y%m%d %H:%M:%S")
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
- entry_dt = dt.datetime.strptime(prior_biz_date_str + " 15:29:00", "%Y%m%d %H:%M:%S")
817
- attribution_day_market_close = dt.datetime.strptime(attribution_date_str + " 15:30:00", "%Y%m%d %H:%M:%S")
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 = dt.datetime.strptime(prior_date_str + " 15:29:00", "%Y%m%d %H:%M:%S")
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
- mtm_date_1530 = mtm_date_str + " 15:30:00"
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
- mtm_date_dt = parse_datetime(mtm_date_1530)
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 = dt.time(15, 30, 0)
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 = dt.datetime.strptime(mtm_date_str + " 15:29:00", "%Y%m%d %H:%M:%S")
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 = dt.datetime.strptime(mtm_date_str + " 15:29:00", "%Y%m%d %H:%M:%S")
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 = "15:30:00",
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: 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 get_config
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 = "09:15:00",
1686
- market_close_time: str = "15:30:00",
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 (market_open <= ts.time() < market_close):
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(market_open_time).time()
1857
- market_close_t = pd.to_datetime(market_close_time).time()
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,