quantplay 2.1.136__tar.gz → 2.1.138__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.136 → quantplay-2.1.138}/PKG-INFO +1 -1
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/broker_factory.py +4 -3
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/generics/broker.py +11 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/jainam_xts.py +11 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils_v2/ConnectV2.py +3 -2
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/PKG-INFO +1 -1
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/SOURCES.txt +2 -1
- {quantplay-2.1.136 → quantplay-2.1.138}/setup.py +1 -1
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_jainam_xts.py +2 -0
- quantplay-2.1.138/tests/test_sub_client_all_types.py +151 -0
- quantplay-2.1.138/tests/test_xts_v2_modify_order.py +37 -0
- quantplay-2.1.136/tests/test_instrument_freshness.py +0 -347
- {quantplay-2.1.136 → quantplay-2.1.138}/README.md +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/pyproject.toml +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue_utils/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue_utils/aliceblue_oauth.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/aliceblue_v2.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/angelone.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/angelone_utils/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/angelone_utils/angeloneWS.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/auto_login/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/auto_login/aliceblue.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/breeze/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/breeze/breeze_utils.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/dhan.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/finvasia_utils/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/finvasia_utils/fa_noren.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/finvasia_utils/finvasia_oauth.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/five_paisa.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/flattrade.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/ft_utils/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/ft_utils/flattrade_utils.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/ft_utils/ft_noren.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/generics/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/icici_direct.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/iifl_xts.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kite_utils.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak_utils/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak_utils/kotak_ws.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/kotak_utils/kotak_ws_lib.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/motilal.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/noren.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/shoonya.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/uplink/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/uplink/uplink_utils.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/upstox.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/Connect.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/Exception.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/InteractiveSocketClient.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils_v2/InteractiveSocketClientV2.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/xts_utils_v2/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/broker/zerodha.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/core/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/core/strategy.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/exception/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/exception/exceptions.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/indicator/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/indicator/iv.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/broker.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/broker_response.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/generics.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/instrument_data.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/model/order_event.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/py.typed +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/strategy/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/strategy/iv_spike.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/strategy/obuy.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/caching.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/constant.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/exchange.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/number_utils.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/pickle_utils.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/redaction.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/utils/selenium_utils.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/aws/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/aws/s3.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay/wrapper/redis.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/dependency_links.txt +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/requires.txt +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/quantplay.egg-info/top_level.txt +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/setup.cfg +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/conftest.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_deltax_positions.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_deltax_root_url.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_kotak_order_feed_url.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/test_log_redaction.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/wrapper/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/wrapper/aws/__init__.py +0 -0
- {quantplay-2.1.136 → quantplay-2.1.138}/tests/wrapper/aws/s3_test.py +0 -0
|
@@ -497,12 +497,13 @@ class BrokerFactory:
|
|
|
497
497
|
)
|
|
498
498
|
broker_data["user_id"] = broker_client.wrapper.userID
|
|
499
499
|
|
|
500
|
-
# Type C dealers must place orders as this sub-client.
|
|
500
|
+
# Type C dealers must place orders as this sub-client. Captured for
|
|
501
|
+
# every XTS type so reclassifying an account to C later works. The login
|
|
501
502
|
# response is its only reliable source, so persist it for sessions
|
|
502
503
|
# later restored from the stored wrapper (the dealer orderbook
|
|
503
504
|
# fallback is empty every morning until the first order exists).
|
|
504
|
-
if broker_client.
|
|
505
|
-
broker_data["sub_client_id"] = broker_client.
|
|
505
|
+
if broker_client.resolved_sub_client_id:
|
|
506
|
+
broker_data["sub_client_id"] = broker_client.resolved_sub_client_id
|
|
506
507
|
|
|
507
508
|
broker_data["wrapper"] = codecs.encode(
|
|
508
509
|
pickle.dumps(broker_client.wrapper), "base64"
|
|
@@ -910,6 +910,17 @@ class Broker(ABC):
|
|
|
910
910
|
sleep_time: float = 10,
|
|
911
911
|
max_modification_count: int = 10,
|
|
912
912
|
) -> None:
|
|
913
|
+
# TODO: think hard about how an exit that won't fill should end. Today:
|
|
914
|
+
# - skip_first_sleep is only cleared inside the branch it guards, so the
|
|
915
|
+
# loop never sleeps and its re-prices run back to back.
|
|
916
|
+
# - Once the count exceeds max_modification_count it modifies to MARKET,
|
|
917
|
+
# which brokers refuse from the API.
|
|
918
|
+
# - The "> 20" cancel only runs when max_modification_count is above 20.
|
|
919
|
+
# Every caller passes 10 or less, so in practice the loop has no exit
|
|
920
|
+
# if the order never completes.
|
|
921
|
+
# Widening the LIMIT buffer instead isn't a fix: exchange price bands
|
|
922
|
+
# reject prices too far from the LTP. In practice exits fill within the
|
|
923
|
+
# first few re-prices, so this is left as is for now.
|
|
913
924
|
modification_count = {}
|
|
914
925
|
skip_first_sleep = True
|
|
915
926
|
while 1:
|
|
@@ -131,6 +131,17 @@ class Jainam(XTS):
|
|
|
131
131
|
|
|
132
132
|
self.ORDER_POLLING_INTERVAL = 5
|
|
133
133
|
|
|
134
|
+
@property
|
|
135
|
+
def resolved_sub_client_id(self) -> str | None:
|
|
136
|
+
"""The sub-client code discovered for this account, if any.
|
|
137
|
+
|
|
138
|
+
The login response carries `clientCodes` for every XTS type, but only
|
|
139
|
+
type C dealers resolve it into `dealer_client_id`. Exposing both means
|
|
140
|
+
generate_token can persist the code for A and B accounts too, so an
|
|
141
|
+
account later reclassified as type C already has what it needs.
|
|
142
|
+
"""
|
|
143
|
+
return self.dealer_client_id or self._login_client_code
|
|
144
|
+
|
|
134
145
|
@property
|
|
135
146
|
def order_client_id(self) -> str:
|
|
136
147
|
"""Client ID for write operations.
|
|
@@ -406,7 +406,7 @@ class XTSConnectV2(XTSCommon):
|
|
|
406
406
|
|
|
407
407
|
def modify_order(
|
|
408
408
|
self,
|
|
409
|
-
appOrderID: int,
|
|
409
|
+
appOrderID: int | str,
|
|
410
410
|
modifiedProductType: XTSTypes.ProductType,
|
|
411
411
|
modifiedOrderType: XTSTypes.OrderType,
|
|
412
412
|
modifiedOrderQuantity: int,
|
|
@@ -420,7 +420,8 @@ class XTSConnectV2(XTSCommon):
|
|
|
420
420
|
"""The facility to modify your open orders by allowing you to change limit order to market or vice versa,
|
|
421
421
|
change Price or Quantity of the limit open order, change disclosed quantity or stop-loss of any
|
|
422
422
|
open stop loss order."""
|
|
423
|
-
appOrderID
|
|
423
|
+
# order.modify rejects a numeric appOrderID ("must be a string")
|
|
424
|
+
appOrderID = str(appOrderID)
|
|
424
425
|
params: dict[str, Any] = {
|
|
425
426
|
"appOrderID": appOrderID,
|
|
426
427
|
"modifiedProductType": modifiedProductType,
|
|
@@ -86,10 +86,11 @@ 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
|
|
90
89
|
tests/test_jainam_xts.py
|
|
91
90
|
tests/test_kotak_order_feed_url.py
|
|
92
91
|
tests/test_log_redaction.py
|
|
92
|
+
tests/test_sub_client_all_types.py
|
|
93
|
+
tests/test_xts_v2_modify_order.py
|
|
93
94
|
tests/wrapper/__init__.py
|
|
94
95
|
tests/wrapper/aws/__init__.py
|
|
95
96
|
tests/wrapper/aws/s3_test.py
|
|
@@ -175,6 +175,7 @@ def test_generate_token_persists_sub_client_id_in_broker_data(
|
|
|
175
175
|
wrapper=types.SimpleNamespace(userID=DEALER_ID),
|
|
176
176
|
md_wrapper=types.SimpleNamespace(),
|
|
177
177
|
dealer_client_id=SUB_CLIENT_ID,
|
|
178
|
+
resolved_sub_client_id=SUB_CLIENT_ID,
|
|
178
179
|
)
|
|
179
180
|
monkeypatch.setattr(broker_factory, "Jainam", lambda **kwargs: stub)
|
|
180
181
|
|
|
@@ -209,6 +210,7 @@ def test_generate_token_omits_sub_client_id_when_unresolved(
|
|
|
209
210
|
wrapper=types.SimpleNamespace(userID=DEALER_ID),
|
|
210
211
|
md_wrapper=types.SimpleNamespace(),
|
|
211
212
|
dealer_client_id=None,
|
|
213
|
+
resolved_sub_client_id=None,
|
|
212
214
|
)
|
|
213
215
|
monkeypatch.setattr(broker_factory, "Jainam", lambda **kwargs: stub)
|
|
214
216
|
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""The sub-client code must be captured for every XTS type.
|
|
2
|
+
|
|
3
|
+
Jainam resolves it into dealer_client_id only for type C dealers, but the
|
|
4
|
+
login response carries clientCodes for A and B too. generate_token used to
|
|
5
|
+
persist only dealer_client_id, so an account added as A or B had no
|
|
6
|
+
sub_client_id stored - and reclassifying it to C later (which is exactly
|
|
7
|
+
what a mis-answered "Account Type" leads to) left it unable to place an
|
|
8
|
+
order until someone hand-wrote the field.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import types
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from quantplay.broker import broker_factory, jainam_xts
|
|
17
|
+
from quantplay.broker.jainam_xts import Jainam
|
|
18
|
+
|
|
19
|
+
SUB = "JSR129"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FakeConnect:
|
|
23
|
+
"""Stands in for XTSConnectV2 so __init__ runs the real login path."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, apiKey: str, secretKey: str, root: str) -> None:
|
|
26
|
+
self.root = root
|
|
27
|
+
self.token = None
|
|
28
|
+
self.userID = "JSR129A77"
|
|
29
|
+
|
|
30
|
+
def session_login(self, root: str) -> dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"type": "success",
|
|
33
|
+
"result": {
|
|
34
|
+
"token": "t",
|
|
35
|
+
"userID": "JSR129A77",
|
|
36
|
+
"isInvestorClient": False,
|
|
37
|
+
"clientCodes": [SUB],
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def marketdata_login(self) -> dict[str, Any]:
|
|
42
|
+
return {"type": "success", "result": {"token": "m"}}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def login_jainam(monkeypatch: Any, **kwargs: Any) -> Jainam:
|
|
46
|
+
monkeypatch.setattr(jainam_xts, "XTSConnectV2", FakeConnect)
|
|
47
|
+
return Jainam(
|
|
48
|
+
api_key="k",
|
|
49
|
+
api_secret="s",
|
|
50
|
+
md_api_key="mk",
|
|
51
|
+
md_api_secret="ms",
|
|
52
|
+
load_instrument=False,
|
|
53
|
+
**kwargs,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("xts_type", ["A", "B", "C"])
|
|
58
|
+
def test_login_client_code_is_exposed_for_every_xts_type(
|
|
59
|
+
monkeypatch: Any, xts_type: str
|
|
60
|
+
):
|
|
61
|
+
broker = login_jainam(monkeypatch, XTS_type=xts_type, is_dealer=True)
|
|
62
|
+
|
|
63
|
+
assert broker.resolved_sub_client_id == SUB
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_type_c_still_prefers_the_resolved_dealer_client(monkeypatch: Any):
|
|
67
|
+
broker = login_jainam(monkeypatch, XTS_type="C", is_dealer=True)
|
|
68
|
+
|
|
69
|
+
assert broker.dealer_client_id == SUB
|
|
70
|
+
assert broker.resolved_sub_client_id == SUB
|
|
71
|
+
assert broker.order_client_id == SUB
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_resolved_sub_client_is_none_when_login_gives_no_client_codes(
|
|
75
|
+
monkeypatch: Any,
|
|
76
|
+
):
|
|
77
|
+
class NoCodes(FakeConnect):
|
|
78
|
+
def session_login(self, root: str) -> dict[str, Any]:
|
|
79
|
+
r = super().session_login(root)
|
|
80
|
+
r["result"].pop("clientCodes")
|
|
81
|
+
return r
|
|
82
|
+
|
|
83
|
+
monkeypatch.setattr(jainam_xts, "XTSConnectV2", NoCodes)
|
|
84
|
+
broker = Jainam(
|
|
85
|
+
api_key="k",
|
|
86
|
+
api_secret="s",
|
|
87
|
+
md_api_key="mk",
|
|
88
|
+
md_api_secret="ms",
|
|
89
|
+
XTS_type="A",
|
|
90
|
+
is_dealer=True,
|
|
91
|
+
load_instrument=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert broker.resolved_sub_client_id is None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize("xts_type", ["A", "B", "C"])
|
|
98
|
+
def test_generate_token_persists_the_sub_client_for_every_type(
|
|
99
|
+
monkeypatch: Any, xts_type: str
|
|
100
|
+
):
|
|
101
|
+
stub = types.SimpleNamespace(
|
|
102
|
+
wrapper=types.SimpleNamespace(userID="JSR129A77"),
|
|
103
|
+
md_wrapper=types.SimpleNamespace(),
|
|
104
|
+
dealer_client_id=SUB if xts_type == "C" else None,
|
|
105
|
+
resolved_sub_client_id=SUB,
|
|
106
|
+
)
|
|
107
|
+
monkeypatch.setattr(broker_factory, "Jainam", lambda **kw: stub)
|
|
108
|
+
|
|
109
|
+
result = broker_factory.BrokerFactory().generate_token(
|
|
110
|
+
{
|
|
111
|
+
"username": "SUNILYADAV",
|
|
112
|
+
"nickname": "JSR129A77",
|
|
113
|
+
"broker": "Jainam",
|
|
114
|
+
"broker_data": {
|
|
115
|
+
"api_key": "k",
|
|
116
|
+
"api_secret": "s",
|
|
117
|
+
"md_api_key": "mk",
|
|
118
|
+
"md_api_secret": "ms",
|
|
119
|
+
"is_dealer": True,
|
|
120
|
+
"XTS_type": xts_type,
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
assert result["broker_data"]["sub_client_id"] == SUB
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_generate_token_omits_the_key_when_nothing_resolved(monkeypatch: Any):
|
|
129
|
+
stub = types.SimpleNamespace(
|
|
130
|
+
wrapper=types.SimpleNamespace(userID="X"),
|
|
131
|
+
md_wrapper=types.SimpleNamespace(),
|
|
132
|
+
dealer_client_id=None,
|
|
133
|
+
resolved_sub_client_id=None,
|
|
134
|
+
)
|
|
135
|
+
monkeypatch.setattr(broker_factory, "Jainam", lambda **kw: stub)
|
|
136
|
+
|
|
137
|
+
result = broker_factory.BrokerFactory().generate_token(
|
|
138
|
+
{
|
|
139
|
+
"username": "U",
|
|
140
|
+
"nickname": "N",
|
|
141
|
+
"broker": "Jainam",
|
|
142
|
+
"broker_data": {
|
|
143
|
+
"api_key": "k",
|
|
144
|
+
"api_secret": "s",
|
|
145
|
+
"md_api_key": "mk",
|
|
146
|
+
"md_api_secret": "ms",
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
assert "sub_client_id" not in result["broker_data"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import MagicMock
|
|
3
|
+
|
|
4
|
+
from quantplay.broker.xts_utils_v2.ConnectV2 import XTSConnectV2
|
|
5
|
+
|
|
6
|
+
APP_ORDER_ID = "18001263010000042909"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def modify_body(app_order_id: int | str) -> dict:
|
|
10
|
+
connect = object.__new__(XTSConnectV2)
|
|
11
|
+
connect.isInvestorClient = False
|
|
12
|
+
connect._put = MagicMock(return_value={"type": "success"})
|
|
13
|
+
|
|
14
|
+
connect.modify_order(
|
|
15
|
+
appOrderID=app_order_id,
|
|
16
|
+
modifiedProductType="NRML",
|
|
17
|
+
modifiedOrderType="Limit",
|
|
18
|
+
modifiedOrderQuantity=90,
|
|
19
|
+
modifiedDisclosedQuantity=0,
|
|
20
|
+
modifiedLimitPrice=874.0,
|
|
21
|
+
modifiedStopPrice=0,
|
|
22
|
+
modifiedTimeInForce="DAY",
|
|
23
|
+
orderUniqueIdentifier="JSR129A43",
|
|
24
|
+
clientID="JSR129",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
route, body = connect._put.call_args.args
|
|
28
|
+
assert route == "order.modify"
|
|
29
|
+
return json.loads(body)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_modify_order_sends_app_order_id_as_string():
|
|
33
|
+
assert modify_body(APP_ORDER_ID)["appOrderID"] == APP_ORDER_ID
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_modify_order_stringifies_an_int_app_order_id():
|
|
37
|
+
assert modify_body(int(APP_ORDER_ID))["appOrderID"] == APP_ORDER_ID
|
|
@@ -1,347 +0,0 @@
|
|
|
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)
|
|
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.136 → quantplay-2.1.138}/quantplay/broker/xts_utils/InteractiveSocketClient.py
RENAMED
|
File without changes
|
|
File without changes
|
{quantplay-2.1.136 → quantplay-2.1.138}/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
|