quantplay 2.1.135__tar.gz → 2.1.136__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 (97) hide show
  1. {quantplay-2.1.135 → quantplay-2.1.136}/PKG-INFO +1 -1
  2. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak_utils/kotak_ws.py +27 -5
  3. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/PKG-INFO +1 -1
  4. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/SOURCES.txt +2 -0
  5. {quantplay-2.1.135 → quantplay-2.1.136}/setup.py +1 -1
  6. quantplay-2.1.136/tests/test_instrument_freshness.py +347 -0
  7. quantplay-2.1.136/tests/test_kotak_order_feed_url.py +39 -0
  8. {quantplay-2.1.135 → quantplay-2.1.136}/README.md +0 -0
  9. {quantplay-2.1.135 → quantplay-2.1.136}/pyproject.toml +0 -0
  10. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/__init__.py +0 -0
  11. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/__init__.py +0 -0
  12. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue.py +0 -0
  13. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue_utils/__init__.py +0 -0
  14. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue_utils/aliceblue_oauth.py +0 -0
  15. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue_v2.py +0 -0
  16. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/angelone.py +0 -0
  17. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/angelone_utils/__init__.py +0 -0
  18. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/angelone_utils/angeloneWS.py +0 -0
  19. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/auto_login/__init__.py +0 -0
  20. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/auto_login/aliceblue.py +0 -0
  21. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/breeze/__init__.py +0 -0
  22. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/breeze/breeze_utils.py +0 -0
  23. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/broker_factory.py +0 -0
  24. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/dhan.py +0 -0
  25. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/finvasia_utils/__init__.py +0 -0
  26. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/finvasia_utils/fa_noren.py +0 -0
  27. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/finvasia_utils/finvasia_oauth.py +0 -0
  28. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/five_paisa.py +0 -0
  29. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/flattrade.py +0 -0
  30. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/ft_utils/__init__.py +0 -0
  31. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/ft_utils/flattrade_utils.py +0 -0
  32. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/ft_utils/ft_noren.py +0 -0
  33. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/generics/__init__.py +0 -0
  34. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/generics/broker.py +0 -0
  35. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/icici_direct.py +0 -0
  36. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/iifl_xts.py +0 -0
  37. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/jainam_xts.py +0 -0
  38. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kite_utils.py +0 -0
  39. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak.py +0 -0
  40. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak_utils/__init__.py +0 -0
  41. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak_utils/kotak_ws_lib.py +0 -0
  42. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/motilal.py +0 -0
  43. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/noren.py +0 -0
  44. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/shoonya.py +0 -0
  45. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/uplink/__init__.py +0 -0
  46. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/uplink/uplink_utils.py +0 -0
  47. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/upstox.py +0 -0
  48. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts.py +0 -0
  49. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/Connect.py +0 -0
  50. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/Exception.py +0 -0
  51. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/InteractiveSocketClient.py +0 -0
  52. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/__init__.py +0 -0
  53. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils_v2/ConnectV2.py +0 -0
  54. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils_v2/InteractiveSocketClientV2.py +0 -0
  55. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils_v2/__init__.py +0 -0
  56. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/zerodha.py +0 -0
  57. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/core/__init__.py +0 -0
  58. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/core/strategy.py +0 -0
  59. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/exception/__init__.py +0 -0
  60. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/exception/exceptions.py +0 -0
  61. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/indicator/__init__.py +0 -0
  62. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/indicator/iv.py +0 -0
  63. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/__init__.py +0 -0
  64. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/broker.py +0 -0
  65. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/broker_response.py +0 -0
  66. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/generics.py +0 -0
  67. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/instrument_data.py +0 -0
  68. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/order_event.py +0 -0
  69. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/py.typed +0 -0
  70. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/strategy/__init__.py +0 -0
  71. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/strategy/iv_spike.py +0 -0
  72. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/strategy/obuy.py +0 -0
  73. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/__init__.py +0 -0
  74. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/caching.py +0 -0
  75. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/constant.py +0 -0
  76. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/exchange.py +0 -0
  77. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/number_utils.py +0 -0
  78. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/pickle_utils.py +0 -0
  79. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/redaction.py +0 -0
  80. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/selenium_utils.py +0 -0
  81. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/__init__.py +0 -0
  82. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/aws/__init__.py +0 -0
  83. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/aws/s3.py +0 -0
  84. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/redis.py +0 -0
  85. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/dependency_links.txt +0 -0
  86. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/requires.txt +0 -0
  87. {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/top_level.txt +0 -0
  88. {quantplay-2.1.135 → quantplay-2.1.136}/setup.cfg +0 -0
  89. {quantplay-2.1.135 → quantplay-2.1.136}/tests/__init__.py +0 -0
  90. {quantplay-2.1.135 → quantplay-2.1.136}/tests/conftest.py +0 -0
  91. {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_deltax_positions.py +0 -0
  92. {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_deltax_root_url.py +0 -0
  93. {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_jainam_xts.py +0 -0
  94. {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_log_redaction.py +0 -0
  95. {quantplay-2.1.135 → quantplay-2.1.136}/tests/wrapper/__init__.py +0 -0
  96. {quantplay-2.1.135 → quantplay-2.1.136}/tests/wrapper/aws/__init__.py +0 -0
  97. {quantplay-2.1.135 → quantplay-2.1.136}/tests/wrapper/aws/s3_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantplay
3
- Version: 2.1.135
3
+ Version: 2.1.136
4
4
  Summary: This python package will be stored in AWS CodeArtifact
5
5
  Home-page:
6
6
  Author:
@@ -10,6 +10,32 @@ from quantplay.broker.kotak_utils.kotak_ws_lib import HSIWebSocket
10
10
 
11
11
  ORDER_FEED_URL = "wss://mis.kotaksecurities.com/realtime"
12
12
  ORDER_FEED_URL_ADC = "wss://cis.kotaksecurities.com/realtime"
13
+ ORDER_FEED_URL_E21 = "wss://e21.kotaksecurities.com/realtime"
14
+ ORDER_FEED_URL_E22 = "wss://e22.kotaksecurities.com/realtime"
15
+ ORDER_FEED_URL_E41 = "wss://e41.kotaksecurities.com/realtime"
16
+ ORDER_FEED_URL_E43 = "wss://e43.kotaksecurities.com/realtime"
17
+
18
+ # The order feed is served per data centre. Only "adc" used to be mapped, so
19
+ # every other data centre silently fell back to the generic endpoint: the
20
+ # socket connects, the CONNECTION frame is acknowledged, and then no order
21
+ # event is ever delivered (the heartbeat is rejected too). Verified against a
22
+ # live dataCenter="E41" account on 2026-09-01 - identical orders produced 0
23
+ # events on the default URL and the full lifecycle on e41. Mapping matches
24
+ # neo_api_client.NeoWebSocket.start_hsi_websocket.
25
+ DATA_CENTER_ORDER_FEED_URLS = {
26
+ "adc": ORDER_FEED_URL_ADC,
27
+ "e21": ORDER_FEED_URL_E21,
28
+ "e22": ORDER_FEED_URL_E22,
29
+ "e41": ORDER_FEED_URL_E41,
30
+ "e43": ORDER_FEED_URL_E43,
31
+ }
32
+
33
+
34
+ def order_feed_url(data_center: str | None) -> str:
35
+ if not data_center:
36
+ return ORDER_FEED_URL
37
+
38
+ return DATA_CENTER_ORDER_FEED_URLS.get(data_center.lower(), ORDER_FEED_URL)
13
39
 
14
40
 
15
41
  class NeoWebSocket:
@@ -96,11 +122,7 @@ class NeoWebSocket:
96
122
  self.hsiWebsocket.send(json.dumps(payload))
97
123
 
98
124
  def start_hsi_websocket(self) -> None:
99
- url = ORDER_FEED_URL
100
-
101
- if self.data_center:
102
- if self.data_center.lower() == "adc":
103
- url = ORDER_FEED_URL_ADC
125
+ url = order_feed_url(self.data_center)
104
126
 
105
127
  self.hsiWebsocket = HSIWebSocket()
106
128
  self.hsiWebsocket.open_connection(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantplay
3
- Version: 2.1.135
3
+ Version: 2.1.136
4
4
  Summary: This python package will be stored in AWS CodeArtifact
5
5
  Home-page:
6
6
  Author:
@@ -86,7 +86,9 @@ tests/__init__.py
86
86
  tests/conftest.py
87
87
  tests/test_deltax_positions.py
88
88
  tests/test_deltax_root_url.py
89
+ tests/test_instrument_freshness.py
89
90
  tests/test_jainam_xts.py
91
+ tests/test_kotak_order_feed_url.py
90
92
  tests/test_log_redaction.py
91
93
  tests/wrapper/__init__.py
92
94
  tests/wrapper/aws/__init__.py
@@ -21,7 +21,7 @@ requirements = [
21
21
  setup(
22
22
  name="quantplay",
23
23
  long_description=Path("README.md").read_text(),
24
- version="2.1.135",
24
+ version="2.1.136",
25
25
  install_requires=requirements,
26
26
  tests_require=[],
27
27
  packages=find_packages(),
@@ -0,0 +1,347 @@
1
+ import os
2
+ import time
3
+ import uuid
4
+ from datetime import datetime, timedelta, timezone
5
+
6
+ import pandas as pd
7
+ import pytest
8
+
9
+ import quantplay.utils.pickle_utils as pickle_utils_module
10
+ from quantplay.broker.broker_factory import (
11
+ Broker as BrokerName,
12
+ )
13
+ from quantplay.broker.broker_factory import (
14
+ BrokerFactory,
15
+ broker_instruments_map,
16
+ instrument_cache,
17
+ )
18
+ from quantplay.broker.generics.broker import Broker
19
+ from quantplay.broker.zerodha import Zerodha
20
+ from quantplay.exception.exceptions import StaleDataFound
21
+ from quantplay.utils.pickle_utils import InstrumentData, PickleUtils
22
+ from quantplay.wrapper.aws import s3 as s3_module
23
+ from quantplay.wrapper.aws.s3 import S3Utils
24
+
25
+
26
+ def unique_name(prefix: str) -> str:
27
+ return f"{prefix}_{uuid.uuid4().hex[:10]}"
28
+
29
+
30
+ class DummyBroker(Broker):
31
+ def get_exchange(self, exchange):
32
+ return exchange
33
+
34
+
35
+ DummyBroker.__abstractmethods__ = frozenset()
36
+
37
+
38
+ class FakeS3Client:
39
+ def __init__(self, response=None, error=None):
40
+ self.response = response
41
+ self.error = error
42
+
43
+ def head_object(self, Bucket, Key):
44
+ if self.error is not None:
45
+ raise self.error
46
+ return self.response
47
+
48
+
49
+ @pytest.fixture
50
+ def cleanup_paths():
51
+ paths = []
52
+ yield paths
53
+ for path in paths:
54
+ if os.path.exists(path):
55
+ os.remove(path)
56
+
57
+
58
+ @pytest.fixture(autouse=True)
59
+ def clear_instrument_cache():
60
+ instrument_cache.clear()
61
+ yield
62
+ instrument_cache.clear()
63
+
64
+
65
+ def make_pickle(file_name: str, data: dict) -> str:
66
+ PickleUtils.save_data(data, file_name)
67
+ return f"/tmp/{file_name}.pickle"
68
+
69
+
70
+ OLD_SYMBOL_DATA = {
71
+ "BFO:OLDSYMBOL": {
72
+ "broker_symbol": "OLDSYMBOL",
73
+ "tradingsymbol": "OLDSYMBOL",
74
+ "lot_size": 20,
75
+ }
76
+ }
77
+
78
+ NEW_SYMBOL_DATA = {
79
+ "BFO:SENSEX2671678400CE": {
80
+ "broker_symbol": "SENSEX2671678400CE",
81
+ "tradingsymbol": "SENSEX2671678400CE",
82
+ "lot_size": 20,
83
+ }
84
+ }
85
+
86
+
87
+ class TestS3LastModified:
88
+ def test_returns_datetime_on_success(self, monkeypatch):
89
+ expected = datetime(2026, 7, 16, 3, 16, tzinfo=timezone.utc)
90
+ monkeypatch.setattr(
91
+ s3_module.boto3,
92
+ "client",
93
+ lambda service: FakeS3Client(response={"LastModified": expected}),
94
+ )
95
+ assert S3Utils.last_modified("bucket", "key") == expected
96
+
97
+ def test_returns_none_on_failure(self, monkeypatch):
98
+ monkeypatch.setattr(
99
+ s3_module.boto3,
100
+ "client",
101
+ lambda service: FakeS3Client(error=Exception("404 Not Found")),
102
+ )
103
+ assert S3Utils.last_modified("bucket", "key") is None
104
+
105
+
106
+ class TestPickleUtils:
107
+ def test_save_data_is_atomic_on_failure(self, monkeypatch, cleanup_paths):
108
+ file_name = unique_name("atomic")
109
+ cleanup_paths.append(make_pickle(file_name, {"a": 1}))
110
+
111
+ def boom(*args, **kwargs):
112
+ raise RuntimeError("disk full")
113
+
114
+ monkeypatch.setattr(pickle_utils_module.pickle, "dump", boom)
115
+ with pytest.raises(Exception):
116
+ PickleUtils.save_data({"b": 2}, file_name)
117
+ monkeypatch.undo()
118
+
119
+ assert PickleUtils.load_data(file_name) == {"a": 1}
120
+
121
+ def test_instrument_data_reloads_when_pickle_changes(self, cleanup_paths):
122
+ file_name = unique_name("reload")
123
+ path = make_pickle(file_name, {"NSE:A": {"lot_size": 1}})
124
+ cleanup_paths.append(path)
125
+
126
+ instance = InstrumentData.get_instance()
127
+ assert instance.load_data(file_name) == {"NSE:A": {"lot_size": 1}}
128
+
129
+ make_pickle(file_name, {"NSE:B": {"lot_size": 2}})
130
+ new_time = time.time() + 10
131
+ os.utime(path, (new_time, new_time))
132
+
133
+ assert instance.load_data(file_name) == {"NSE:B": {"lot_size": 2}}
134
+
135
+ def test_instrument_data_serves_memory_copy_when_unchanged(self, cleanup_paths):
136
+ file_name = unique_name("cached")
137
+ path = make_pickle(file_name, {"NSE:A": {"lot_size": 1}})
138
+ cleanup_paths.append(path)
139
+
140
+ instance = InstrumentData.get_instance()
141
+ first = instance.load_data(file_name)
142
+ second = instance.load_data(file_name)
143
+ assert first is second
144
+
145
+
146
+ class TestGetLotSize:
147
+ def test_missing_symbol_raises_stale_data_found(self):
148
+ broker = DummyBroker()
149
+ broker.symbol_data = {}
150
+
151
+ with pytest.raises(StaleDataFound) as exc_info:
152
+ broker.get_lot_size("BFO", "SENSEX2671678400CE")
153
+
154
+ assert "BFO:SENSEX2671678400CE" in str(exc_info.value)
155
+
156
+ def test_returns_lot_size_when_symbol_exists(self):
157
+ broker = DummyBroker()
158
+ broker.symbol_data = NEW_SYMBOL_DATA
159
+
160
+ assert broker.get_lot_size("BFO", "SENSEX2671678400CE") == 20
161
+
162
+
163
+ class TestPickleFreshness:
164
+ def setup_method(self):
165
+ self.factory = BrokerFactory()
166
+
167
+ def test_missing_pickle_is_stale(self, monkeypatch):
168
+ monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
169
+ assert self.factory.is_instrument_pickle_fresh(unique_name("missing")) is False
170
+
171
+ def test_pickle_from_today_with_no_s3_csv_is_fresh(
172
+ self, monkeypatch, cleanup_paths
173
+ ):
174
+ file_name = unique_name("fresh")
175
+ cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
176
+ monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
177
+
178
+ assert self.factory.is_instrument_pickle_fresh(file_name) is True
179
+
180
+ def test_pickle_from_yesterday_is_stale(self, monkeypatch, cleanup_paths):
181
+ file_name = unique_name("stale_date")
182
+ path = make_pickle(file_name, OLD_SYMBOL_DATA)
183
+ cleanup_paths.append(path)
184
+ yesterday = time.time() - 86400
185
+ os.utime(path, (yesterday, yesterday))
186
+ monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
187
+
188
+ assert self.factory.is_instrument_pickle_fresh(file_name) is False
189
+
190
+ def test_pickle_older_than_s3_csv_is_stale(self, monkeypatch, cleanup_paths):
191
+ file_name = unique_name("stale_s3")
192
+ cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
193
+ s3_time = datetime.now(timezone.utc) + timedelta(hours=1)
194
+ monkeypatch.setattr(
195
+ S3Utils, "last_modified", staticmethod(lambda b, k: s3_time)
196
+ )
197
+
198
+ assert self.factory.is_instrument_pickle_fresh(file_name) is False
199
+
200
+ def test_pickle_newer_than_s3_csv_is_fresh(self, monkeypatch, cleanup_paths):
201
+ file_name = unique_name("fresh_s3")
202
+ cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
203
+ s3_time = datetime.now(timezone.utc) - timedelta(hours=2)
204
+ monkeypatch.setattr(
205
+ S3Utils, "last_modified", staticmethod(lambda b, k: s3_time)
206
+ )
207
+
208
+ assert self.factory.is_instrument_pickle_fresh(file_name) is True
209
+
210
+
211
+ class TestSetBrokerInstruments:
212
+ def setup_method(self):
213
+ self.factory = BrokerFactory()
214
+
215
+ def _stale_pickle_setup(self, monkeypatch, cleanup_paths):
216
+ file_name = unique_name("alicebluev2_test")
217
+ monkeypatch.setitem(
218
+ broker_instruments_map, BrokerName.ALICEBLUE_V2, file_name
219
+ )
220
+ path = make_pickle(file_name, OLD_SYMBOL_DATA)
221
+ cleanup_paths.append(path)
222
+ yesterday = time.time() - 86400
223
+ os.utime(path, (yesterday, yesterday))
224
+ monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
225
+ return file_name
226
+
227
+ def test_stale_pickle_triggers_refresh(self, monkeypatch, cleanup_paths):
228
+ file_name = self._stale_pickle_setup(monkeypatch, cleanup_paths)
229
+ broker = DummyBroker()
230
+ refresh_calls = []
231
+
232
+ def fake_refresh(name):
233
+ refresh_calls.append(name)
234
+ broker.symbol_data = NEW_SYMBOL_DATA
235
+ broker.initialize_broker_symbol_map()
236
+
237
+ monkeypatch.setattr(broker, "refresh_instruments", fake_refresh)
238
+
239
+ result = self.factory.set_broker_instruments(
240
+ broker_name=BrokerName.ALICEBLUE_V2, broker=broker
241
+ )
242
+
243
+ assert refresh_calls == [file_name]
244
+ assert "BFO:SENSEX2671678400CE" in result.symbol_data
245
+ assert (
246
+ instrument_cache.get(f"{BrokerName.ALICEBLUE_V2}_instruments")
247
+ == NEW_SYMBOL_DATA
248
+ )
249
+
250
+ def test_refresh_failure_serves_stale_pickle(self, monkeypatch, cleanup_paths):
251
+ self._stale_pickle_setup(monkeypatch, cleanup_paths)
252
+ broker = DummyBroker()
253
+
254
+ def fake_refresh(name):
255
+ raise Exception("s3 is down")
256
+
257
+ monkeypatch.setattr(broker, "refresh_instruments", fake_refresh)
258
+
259
+ result = self.factory.set_broker_instruments(
260
+ broker_name=BrokerName.ALICEBLUE_V2, broker=broker
261
+ )
262
+
263
+ assert "BFO:OLDSYMBOL" in result.symbol_data
264
+
265
+ def test_fresh_pickle_skips_refresh(self, monkeypatch, cleanup_paths):
266
+ file_name = unique_name("alicebluev2_test")
267
+ monkeypatch.setitem(
268
+ broker_instruments_map, BrokerName.ALICEBLUE_V2, file_name
269
+ )
270
+ cleanup_paths.append(make_pickle(file_name, OLD_SYMBOL_DATA))
271
+ monkeypatch.setattr(S3Utils, "last_modified", staticmethod(lambda b, k: None))
272
+
273
+ broker = DummyBroker()
274
+
275
+ def fake_refresh(name):
276
+ raise AssertionError("refresh_instruments should not be called")
277
+
278
+ monkeypatch.setattr(broker, "refresh_instruments", fake_refresh)
279
+
280
+ result = self.factory.set_broker_instruments(
281
+ broker_name=BrokerName.ALICEBLUE_V2, broker=broker
282
+ )
283
+
284
+ assert "BFO:OLDSYMBOL" in result.symbol_data
285
+
286
+
287
+ class TestRefreshInstruments:
288
+ def test_rebuilds_from_s3_and_updates_all_caches(self, monkeypatch, cleanup_paths):
289
+ file_name = unique_name("refresh_base")
290
+ cleanup_paths.append(f"/tmp/{file_name}.pickle")
291
+
292
+ local_csv = f"/tmp/quantplay-market-data/symbol_data/{file_name}.csv"
293
+ os.makedirs(os.path.dirname(local_csv), exist_ok=True)
294
+ with open(local_csv, "w") as f:
295
+ f.write("stale,csv\n")
296
+ cleanup_paths.append(local_csv)
297
+
298
+ df = pd.DataFrame(
299
+ [
300
+ {
301
+ "exchange": "BFO",
302
+ "broker_symbol": "SENSEX2671678400CE",
303
+ "tradingsymbol": "SENSEX2671678400CE",
304
+ "lot_size": 20,
305
+ }
306
+ ]
307
+ )
308
+
309
+ def fake_read_csv(bucket, key):
310
+ assert bucket == "quantplay-market-data"
311
+ assert key == f"symbol_data/{file_name}.csv"
312
+ return df
313
+
314
+ monkeypatch.setattr(S3Utils, "read_csv", staticmethod(fake_read_csv))
315
+
316
+ broker = DummyBroker()
317
+ broker.refresh_instruments(file_name)
318
+
319
+ assert "BFO:SENSEX2671678400CE" in broker.symbol_data
320
+ assert broker.symbol_data["BFO:SENSEX2671678400CE"]["lot_size"] == 20
321
+ assert not os.path.exists(local_csv)
322
+ assert "BFO:SENSEX2671678400CE" in PickleUtils.load_data(file_name)
323
+ assert (
324
+ InstrumentData.get_instance().load_data(file_name) == broker.symbol_data
325
+ )
326
+ assert broker.broker_symbol_map["SENSEX2671678400CE"] == "SENSEX2671678400CE"
327
+
328
+ def test_zerodha_rebuilds_from_live_instruments(self, cleanup_paths):
329
+ file_name = unique_name("zerodha_refresh")
330
+ cleanup_paths.append(f"/tmp/{file_name}.pickle")
331
+
332
+ class FakeWrapper:
333
+ def instruments(self):
334
+ return [
335
+ {
336
+ "exchange": "NFO",
337
+ "tradingsymbol": "NIFTY26JUL25000CE",
338
+ "lot_size": 75,
339
+ }
340
+ ]
341
+
342
+ broker = object.__new__(Zerodha)
343
+ broker.wrapper = FakeWrapper()
344
+ broker.refresh_instruments(file_name)
345
+
346
+ assert broker.symbol_data["NFO:NIFTY26JUL25000CE"]["lot_size"] == 75
347
+ assert "NFO:NIFTY26JUL25000CE" in PickleUtils.load_data(file_name)
@@ -0,0 +1,39 @@
1
+ import pytest
2
+
3
+ from quantplay.broker.kotak_utils.kotak_ws import (
4
+ ORDER_FEED_URL,
5
+ order_feed_url,
6
+ )
7
+
8
+
9
+ @pytest.mark.parametrize(
10
+ ("data_center", "expected"),
11
+ [
12
+ ("adc", "wss://cis.kotaksecurities.com/realtime"),
13
+ ("e21", "wss://e21.kotaksecurities.com/realtime"),
14
+ ("e22", "wss://e22.kotaksecurities.com/realtime"),
15
+ ("e41", "wss://e41.kotaksecurities.com/realtime"),
16
+ ("e43", "wss://e43.kotaksecurities.com/realtime"),
17
+ ],
18
+ )
19
+ def test_each_data_center_gets_its_own_endpoint(data_center: str, expected: str) -> None:
20
+ assert order_feed_url(data_center) == expected
21
+
22
+
23
+ def test_data_center_is_case_insensitive() -> None:
24
+ # UserBrokerAccounts stores it upper-case ("E41"); Kotak's own client
25
+ # lower-cases before comparing.
26
+ assert order_feed_url("E41") == order_feed_url("e41")
27
+
28
+
29
+ @pytest.mark.parametrize("data_center", [None, "", "zz"])
30
+ def test_unknown_or_absent_data_center_falls_back(data_center: str | None) -> None:
31
+ assert order_feed_url(data_center) == ORDER_FEED_URL
32
+
33
+
34
+ def test_non_adc_data_centers_are_not_the_default() -> None:
35
+ # The regression: e21/e22/e41/e43 silently resolved to ORDER_FEED_URL, so
36
+ # the socket connected to a data centre that holds no session for the
37
+ # account and delivered no order events.
38
+ for data_center in ("e21", "e22", "e41", "e43"):
39
+ assert order_feed_url(data_center) != ORDER_FEED_URL
File without changes
File without changes
File without changes