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.
- {quantplay-2.1.135 → quantplay-2.1.136}/PKG-INFO +1 -1
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak_utils/kotak_ws.py +27 -5
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/PKG-INFO +1 -1
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/SOURCES.txt +2 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/setup.py +1 -1
- quantplay-2.1.136/tests/test_instrument_freshness.py +347 -0
- quantplay-2.1.136/tests/test_kotak_order_feed_url.py +39 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/README.md +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/pyproject.toml +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue_utils/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue_utils/aliceblue_oauth.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/aliceblue_v2.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/angelone.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/angelone_utils/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/angelone_utils/angeloneWS.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/auto_login/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/auto_login/aliceblue.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/breeze/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/breeze/breeze_utils.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/broker_factory.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/dhan.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/finvasia_utils/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/finvasia_utils/fa_noren.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/finvasia_utils/finvasia_oauth.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/five_paisa.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/flattrade.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/ft_utils/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/ft_utils/flattrade_utils.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/ft_utils/ft_noren.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/generics/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/generics/broker.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/icici_direct.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/iifl_xts.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/jainam_xts.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kite_utils.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak_utils/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/kotak_utils/kotak_ws_lib.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/motilal.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/noren.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/shoonya.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/uplink/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/uplink/uplink_utils.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/upstox.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/Connect.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/Exception.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/InteractiveSocketClient.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils_v2/ConnectV2.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils_v2/InteractiveSocketClientV2.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils_v2/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/zerodha.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/core/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/core/strategy.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/exception/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/exception/exceptions.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/indicator/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/indicator/iv.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/broker.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/broker_response.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/generics.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/instrument_data.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/model/order_event.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/py.typed +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/strategy/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/strategy/iv_spike.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/strategy/obuy.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/caching.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/constant.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/exchange.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/number_utils.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/pickle_utils.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/redaction.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/utils/selenium_utils.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/aws/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/aws/s3.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay/wrapper/redis.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/dependency_links.txt +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/requires.txt +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/quantplay.egg-info/top_level.txt +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/setup.cfg +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/conftest.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_deltax_positions.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_deltax_root_url.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_jainam_xts.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/test_log_redaction.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/wrapper/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/wrapper/aws/__init__.py +0 -0
- {quantplay-2.1.135 → quantplay-2.1.136}/tests/wrapper/aws/s3_test.py +0 -0
|
@@ -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 =
|
|
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(
|
|
@@ -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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils/InteractiveSocketClient.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quantplay-2.1.135 → quantplay-2.1.136}/quantplay/broker/xts_utils_v2/InteractiveSocketClientV2.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|